The Audit DSOs of the rtld
___ ___
/ _ \ / _ \
__ __| (_) || | | | ___
\ \/ / \__. || | | | / __|
> < / / | |_| || (__
/_/\_\ /_/ \___/ \___|
[toc]
----[ 1. Intro
----[ 2. The Audit DSOs
--------[ 2.1 The Audit DSO Internal
------------[ 2.1.1 The structs of Audit Lists and Interfaces
------------[ 2.1.2 Load an audit DSO
------------[ 2.1.3 Do Lookup The Interfaces
------------[ 2.1.4 Open The Object
--------[ 2.2 audit_dso_example.c: Writing a audit DSO
--------[ 2.3 The vulnerability
----[ 3. Conclusion
----[ 4. References
----[ 5. Greets
----[ 1. Intro
The article covered explanation of The Audit DSOs of Internal
of the rtld and writing a DSO, Lastly The vulnerability.
----[ 2. The Audit DSOs
----[ 2.1 The Audit DSO Internal
The audit DSO loaded by the rtld after running a process. In other
words, It affected all process In the userland. And The audit DSOs
module path passed to an environment of $LD_AUDIT. For Instance
export $LD_AUDIT=libpcrprofile.so.
First, Just See the process of loading audit DSOs.
Auditing DSOs load process:
(1) Called the function to open the auditing DSO.
(2) Do lookup "la_version" symbol and call it.
(3) Do lookup symbols by using auditing Interface name
and call it.
(4) If an Interface is binded, Set 1 to last bit
of the flag of main_map(linkmap object)&dl_rtld_map
(linkmap object)->l_audit[cnt].bindflags.
(5) Setup RTLD debugger with .debug dynamic section.
(6) Called "la_activity" symbol function with the
constant of LA_ACT_ADD to print out the message
of added the auditing Interface object.
* (5), (6) is the rtld debugger with la_activity
Interface of auditing DSOs.
----[ 2.1.1 The structs of Audit Lists and Interfaces
Audit DSO have two structs to load a security module on the rtld.
First struct for the audit lists and the next struct for the audit
Interfaces. The audit lists struct maintained the loaded security
module like an shared object, the struct defined in the global In
the rtld.c rtld source code and the audit Interfaces struct mainta
ined virtual function pointers to Interface for each DSOs.
The audit_list struct gots loaded audit DSO name as the member
variable of *name and *next pointer for the next module, It's a
queue by a single linked list.
*audit_list
.-----------. .----------. .----------.
| old_newp | | old_newp | | newp |
| - *name | | *name | | *name |
| - *next |--->| *next |--->| *next |---+
'-----------' '----------' '----------' |
(first) ^ |
| |
+---------+
(The Last Entry)
The *audit_list variable pointers to the entry for the first loaded
dso and the last entry always for last loaded module.
See the struct!
The audit_list struct In elf/rtld.c:
----snip----snip----snip----snip----snip----snip----
/* List of auditing DSOs. */
static struct audit_list
{
const char *name;
struct audit_list *next;
} *audit_list;
----snip----snip----snip----snip----snip----snip----
The function of process_dl_audit() In elf/rtld.c added an entry to
the queue.
Just See the next, The audit Interfaces struct as follows:
*audit_ifaces (Interfaces)
.---------------. .----------.
| old | | new |
| - (*activity) | | |
| - (*objsearch)| | ... | ... n
| - (*objopen) | | | GL(dl_naudit)=n
| ... | | |
| ... | | |
| - *next |-->| *next |
'---------------' '----------'
The Interfaces are symbols on a shared object and the symbol's
function pointer loaded on the struct of *audit_ifaces by the rtld.
The struct also same queue as the *audit_list and those Intefaces
will called by the rtld to load and operation the Audit DSOs. An
*audit_list entry per an *audit_ifaces entry even though those
structs are not linked for each.
The count of audit Interfaces stored on the rtld's global variable
of GL(dl_naudit).
The audit Interface lookup'd and called as follows:
(1) Lookup la_objopen symbol.
(2) Called the symbol via calling the audit_ifaces->objopen()
function pointer.
And The Auditing Interfaces:
- la_activity DSO Activity Monitor
- la_objsearch Object Search
- la_objopen Object Open
- la_preinit Pre Initialization
- la_symbind32 /
la_symbind64 Symbol Binding
- la_objclose Object Close
----[ 2.1.2 Load an audit DSO
The audit DSOs process codes In the dl_main() of rtld's main
function. rtld called dlmopen_doit() In rtld.c to load the audit
DSOs. And dlmopen_doit() call _dl_open() with the audit DSO path
as first argument, added __RTLD_AUDIT flag to second argument.
It's loaded like the shared object.
The dlmopen_doit() In elf/rtld.c:
----snip----snip----snip----snip----snip----snip----snip----snip----
...
static void
dlmopen_doit (void *a)
{
struct dlmopen_args *args = (struct dlmopen_args *) a;
// If dynamic linked, the return value is 0.
args->map = _dl_open (args->fname, RTLD_LAZY | __RTLD_DLOPEN |
__RTLD_AUDIT,
dl_main, LM_ID_NEWLM, _dl_argc, INTUSE(_dl_argv),
__environ);
}
----snip----snip----snip----snip----snip----snip----snip----snip----
If audit_list variable is exists, entered the load process. And
prepared dlmopen_args struct for the argument and called dlmopen
_doit() to load the dso.
The code of dl_main In elf/rtld.c:
----snip----snip----snip----snip----snip----snip----snip----snip----
static void
dl_main (const ElfW(Phdr) *phdr,
ElfW(Word) phnum,
ElfW(Addr) *user_entry)
{
...
/* If we have auditing DSOs to load, do it now. */
if (__builtin_expect (audit_list != NULL, 0))
{
/*
* Iterate over all entries in the list. The order is important.
*/
struct audit_ifaces *last_audit = NULL;
/* audit_list struct */
struct audit_list *al = audit_list->next;
do
{
...
struct dlmopen_args dlmargs;
/* Set DSO path for the argument. */
dlmargs.fname = al->name;
/* Set the map member variable as NULL. */
dlmargs.map = NULL;
const char *objname;
const char *err_str = NULL;
bool malloced;
/*
* call dlmopen_doit() to load an audit dso!
*/
(void) _dl_catch_error (&objname, &err_str, &malloced,
dlmopen_doit, &dlmargs);
----snip----snip----snip----snip----snip----snip----snip----snip----
Now, _dl_open() loaded the audit DSO passed from the environment of
$LD_AUDIT. The dso's Information loaded on somewhere of the link_map
objects.
----[ 2.1.3 Do Lookup The Interfaces
After a loaded audit dso, the next is to do Lookup the Interfaces
of the module. The lookup_doit() did the lookup a Interface. First
The la_version symbol lookup'd from the ELF object the process
running In userland by using the lookup function. The Interface to
check the Interface version is matched.
The lookup_doit() In elf/rtld.c:
----snip----snip----snip----snip----snip----snip----snip----snip----
static void lookup_doit (void *a)
{
struct lookup_args *args = (struct lookup_args *) a;
const ElfW(Sym) *ref = NULL;
args->result = NULL;
lookup_t l = _dl_lookup_symbol_x (args->name, args->map, &ref,
args->map->l_local_scope, NULL, 0,
DL_LOOKUP_RETURN_NEWEST, NULL);
/* Symbol lookup success? */
/* store the symbol object */
/* on args->result. */
if (ref != NULL)
args->result = DL_SYMBOL_ADDRESS (l, ref);
}
----snip----snip----snip----snip----snip----snip----snip----snip----
Do Lookup la_version symbol, a Interface! The lookup_args struct's
->name member variable gots to lookup Interface name and ->map
gots NULL. The _dl_catch_error() function's 4th argument is
lookup_doit() and 5th argument is &lookup_args. _dl_catch_error()
will called lookup_doit() with an argument as &lookup_args.
The lookup_doit() In elf/rtld.c:
----snip----snip----snip----snip----snip----snip----snip----snip----
struct lookup_args largs; /* argument struct. */
largs.name = "la_version"; /* to lookup Interface name */
largs.map = dlmargs.map; /* largs.map = NULL */
/*
argument = largs.name("la_version").
result = largs.result.
*/
/* Check whether the interface version matches. */
(void) _dl_catch_error (&objname, &err_str, &malloced,
lookup_doit, &largs);
----snip----snip----snip----snip----snip----snip----snip----snip----
After lookup'd la_version Interface, store the address of the
lookup'd symbol that largs.result to laversion function pointer
and call it to check Interface version match. If matched, entered
below block to lookup other Interfaces also.
The lookup_doit() In elf/rtld.c:
----snip----snip----snip----snip----snip----snip----snip----snip----
unsigned int (*laversion) (unsigned int);
unsigned int lav;
if (err_str == NULL &&
(laversion = largs.result) != NULL &&
(lav = laversion (LAV_CURRENT)) > 0 && lav <= LAV_CURRENT)
{
----snip----snip----snip----snip----snip----snip----snip----snip----
The next, do lookup other Interfaces. The *newp union declared In
the code with the member of the audit_ifaces struct, callback
function pointer of the Interface and In the follow code, 6
Interfaces will be lookup'd In a while loop to process a linked
list with the *next pointer of *audit_list.
The Interface of la_objsearch searched symbol on ELF object and the
la_symbind32 or la_symbind64 Interface binding a symbol from an ELF
object.
All the audit Interfaces Implemeneted In the source code of a audit
dso as the test code of elf/tst-auditmod1.c. See the test code.
la_symbind32 / la_symbind64 return the relative addr of the symbol to
bind.
The codes In elf/tst-auditmod1.c:
----snip----snip----snip----snip----snip----snip----snip----snip----
...
uintptr_t
la_symbind32 (Elf32_Sym *sym, unsigned int ndx, uintptr_t *refcook,
uintptr_t *defcook, unsigned int *flags, const char *symname)
{
printf ("symbind32: symname=%s, st_value=%#lx, ndx=%u, flags=
%u\n",
symname, (long int) sym->st_value, ndx, *flags);
return sym->st_value;
}
uintptr_t
la_symbind64 (Elf64_Sym *sym, unsigned int ndx, uintptr_t *refcook,
uintptr_t *defcook, unsigned int *flags, const char *symname)
{
printf ("symbind64: symname=%s, st_value=%#lx, ndx=%u, flags=
%u\n",
symname, (long int) sym->st_value, ndx, *flags);
return sym->st_value;
}
...
----snip----snip----snip----snip----snip----snip----snip----snip----
See the lookup the other Interfaces!
The lookup_doit() In elf/rtld.c:
----snip----snip----snip----snip----snip----snip----snip----snip----
/* Allocate structure for the callback function pointers.
This call can never fail. */
union
{
struct audit_ifaces ifaces;
#define naudit_ifaces 8
void (*fptr[naudit_ifaces]) (void); /* void (*fptr[8])(void); */
} *newp = malloc (sizeof (*newp));
/* Names of the auditing interfaces. All in one long string. */
static const char audit_iface_names[] =
"la_activity\0"
"la_objsearch\0"
"la_objopen\0"
"la_preinit\0"
#if __ELF_NATIVE_CLASS == 32
"la_symbind32\0"
#elif __ELF_NATIVE_CLASS == 64
"la_symbind64\0"
#else
# error "__ELF_NATIVE_CLASS must be defined"
#endif
#define STRING(s) __STRING (s)
"la_" STRING (ARCH_LA_PLTENTER) "\0"
"la_" STRING (ARCH_LA_PLTEXIT) "\0"
"la_objclose\0";
unsigned int cnt = 0;
const char *cp = audit_iface_names;
do
{
largs.name = cp;
(void) _dl_catch_error (&objname, &err_str, &malloced,
lookup_doit, &largs);
/* Store the pointer. */
if (err_str == NULL && largs.result != NULL)
{
newp->fptr[cnt] = largs.result;
/* The dynamic linker link map is statically allocated
initialize the data now. */
GL(dl_rtld_map).l_audit[cnt].cookie =
(intptr_t) &GL(dl_rtld_map);
}
else
newp->fptr[cnt] = NULL;
++cnt;
cp = (char *) rawmemchr (cp, '\0') + 1;
}
while (*cp != '\0');
assert (cnt == naudit_ifaces);
/* Now append the new auditing interface to the list. */
newp->ifaces.next = NULL;
if (last_audit == NULL)
last_audit = GLRO(dl_audit) = &newp->ifaces;
else
last_audit = last_audit->next = &newp->ifaces;
++GLRO(dl_naudit);
/* Mark the DSO as being used for auditing. */
dlmargs.map->l_auditing = 1;
}
else
{
/* We cannot use the DSO, it does not have the
appropriate interfaces or it expects something
more recent. */
#ifndef NDEBUG
Lmid_t ns = dlmargs.map->l_ns;
#endif
_dl_close (dlmargs.map);
/* Make sure the namespace has been cleared entirely. */
assert (GL(dl_ns)[ns]._ns_loaded == NULL);
assert (GL(dl_ns)[ns]._ns_nloaded == 0);
#ifdef USE_TLS
GL(dl_tls_max_dtv_idx) = tls_idx;
#endif
goto not_loaded;
}
}
al = al->next;
}
while (al != audit_list->next);
----snip----snip----snip----snip----snip----snip----snip----snip----
The auditing DSOs marked 1 as In used on ->l_auditing member variable
of dlmargs.map It's declared In codes for open the dso. The .map
member variable of dlmargs gots the pointer for allocated linkmap
object after called dlmopen_doit() for the audit dso. In other words
The ->l_auditing member variable auditing dso's linkmap object
marked as 1 than the process's link_map object.
----[ 2.1.4 Open The Object
Finally, Do Open the object of *afct for the audit_ifaces It will
called the dso's la_open function Implemented by the author of the
auditing dso as can see In the elf/tst-auditmod1.c. The la_open
just print out a message of open this object. The rtld called
la_open functions for each audit DSO.
----snip----snip----snip----snip----snip----snip----snip----snip----
/* If we have any auditing modules, announce that we already
have two objects loaded. */
if (__builtin_expect (GLRO(dl_naudit) > 0, 0))
{
struct link_map *ls[2] = { main_map, &GL(dl_rtld_map) };
for (unsigned int outer = 0; outer < 2; ++outer)
{
struct audit_ifaces *afct = GLRO(dl_audit);
for (unsigned int cnt = 0; cnt < GLRO(dl_naudit); ++cnt)
{
if (afct->objopen != NULL)
{
ls[outer]->l_audit[cnt].bindflags =
afct->objopen (ls[outer], LM_ID_BASE, &ls[outer]->
l_audit[cnt].cookie);
ls[outer]->l_audit_any_plt |= ls[outer]->
l_audit[cnt].bindflags != 0;
}
afct = afct->next; /* move the next audit Interface */
}
}
}
----snip----snip----snip----snip----snip----snip----snip----snip----
As you may know, Audit DSO can be used for automated analyzing for
a monitor In userland, the library layer In the OS architecture.
Writing a audit DSO!
----[ 2.2 Writing a audit DSO
I demostrated a userland monitor via the Audit DSO. It should be
compiled In the rtld source code tree of glibc elf/audit_dso_example.c.
audit_dso_example.c:
----
#include <dlfcn.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <bits/wordsize.h>
#include <gnu/lib-names.h>
unsigned int la_version(unsigned int v){
return v;
}
unsigned int la_objopen(struct link_map *l, Lmid_t lmid,
unsigned int *cookie){
FILE *fp;
fp = fopen("/tmp/audit_dso_example.log", "w+");
if(fp <= NULL){
printf("failed to open audit dso example\n");
fclose(fp);
}
/*
* The link_map Object passed as first argument!
* link_map struct In elf/link.h.
*
*/
fprintf(fp, "-------- audit dso example --------\n");
fprintf(fp, "Executed program name: %s\n ", l->l_name);
fprintf(fp, "Base Addr of the object: %p\n", l->l_addr);
fprintf(fp, "The addr of .dynamic: \n\n ", l->l_ld);
fprintf(fp, "-----------------------------------\n");
/*
* Now, Can resolve the ELF sections of the executed
* program with l->l_ld. do resolve relocation a symbol! [2].
*
*/
fclose(fp);
return 0;
}
void la_preinit(unsigned int *cookie){
return;
}
void la_objclose(unsigned int *cookie){
printf("audit_dso_example: an audit DSO closed.");
return 0;
}
----
----[ 2.3 The vulnerability
The taviso's arbitrary audit DSO load bug [1] demostrated the audit
DSO's security bug:
(1) LD_AUDIT="libpcprofile.so" PCPROFILE_OUTPUT="/etc/cron.d/exploit"
The codes of the libpcprofile.so creates a file that $PCPROFILE_
OUTPUT. libpcprofile.so is not a audit dso and cannot be loaded
the audit dso loaded as a shared object and the code shared
object's executed! It's security bug with a SUID bit.
(2) Execute /bin/ping to create a world writable file of /etc/cron.d
/exploit. The ping SUID bit! and After the ping process running
soon, the rtld loaded the audit dso and created the file.
(3) Setup a crontab and wait to escalate the privilege.
printf "* * * * * root cp /bin/dash /tmp/exploit; chmod u+s
/tmp/exploit\n" > /etc/cron.d/exploit.
Arbitrary shared object can be loaded via audit DSO load and the
audit dso's code executed when after execute a process In userland
If the process gots SUID bit like /bin/ping.
What the discussion of the security bug is that the arbitrary shared
object load via all process In userland execution time. A shared
object also can compiled and loaded with as a user and cannot load
it with the privilege.
See the call path:
----
/bin/ping execute! with SUID bit
|
+-> rtld: audit DSO load (In the execution time)
|
+-> rtld: _dlm_opendoit()
The shared object load with UID 0.
...
----
----[ 3. Conclusion
Teh article covered The Internal of The Audit DSO and Writing a
security module for userland and explained the security bug of
arbitrary DSO load.
The Audit DSO can be used for a automated monitor In the userland
and demostrated it In the audit_dso_example.c. It can be Implemented
The monitor In the execution time with the ELF resolve.
----[ 4. References
[1] Taviso, 2010, Taviso's GNU C library dynamic linker LD_AUDIT.
arbitrary DSO load Vulnerability.
- http://www.exploit-db.com/exploits/15304
[2] x90c, 2012, ELF_linker.c.
- http://www.x90c.org/ELF32_linker.c
----[ 5. Greets
Greets to ... #phrack of efnet ...
... #social of overthewire ...
EOF