您可以使用 rtld-audit 接口访问与进程的运行时链接有关的信息。rtld-audit 接口实现为提供一个或多个审计接口例程的审计库。如果将该库作为进程的一部分装入,则运行时链接程序会在进程执行的不同阶段调用审计例程。审计库可以使用这些接口访问以下各项:
依赖项搜索。可以通过审计库替换搜索路径。
与装入的目标文件相关的信息。
装入的目标文件之间进行的符号绑定。可以通过审计库更改这些绑定。
通过利用过程链接表各项所提供的延迟绑定机制,可以审计函数调用及其返回值。请参见过程链接表(特定于处理器)。可以通过审计库修改函数参数及其返回值。
通过预装入专用的共享目标文件可以获取其中的部分信息。但是,预装入的目标文件与应用程序的目标文件存在于同一名称空间内。这种预装入通常会限制预装入共享目标文件的实现或者使实现变得更为复杂。rtld-audit 接口会为您提供唯一的名称空间,用于在其中执行审计库。此名称空间可确保在应用程序内进行正常绑定时审计库不会侵入。
配置共享目标文件中介绍对共享目标文件的运行时配置即是使用此 rtld-audit 接口的一个示例。
运行时链接程序将动态可执行文件与其依赖项绑定时,会生成链接映射的链接列表,用于对此应用程序进行说明。链接映射结构说明了应用程序中的每个目标文件。/usr/include/sys/link.h 中定义了此链接映射结构。绑定应用程序的目标文件所需的符号搜索机制会遍历此链接映射的列表。此链接映射列表用于提供应用程序符号解析的名称空间。
运行时链接程序也通过链接映射来进行说明。此链接映射以不同于应用程序目标文件列表的列表中进行维护。因此,运行时链接程序驻留在其自己唯一的名称空间中,从而可防止应用程序查看或直接访问运行时链接程序内的任意服务。因此,应用程序只能通过 libc.so.1 或 libdl.so.1 提供的过滤器来访问运行时链接程序。
/usr/include/link.h 中定义了两个标识符,用于定义应用程序和运行时链接程序的链接映射列表:
#define LM_ID_BASE 0 /* application link-map list */ #define LM_ID_LDSO 1 /* runtime linker link-map list */
除了这两个标准的链接映射列表以外,运行时链接程序还允许再创建任意数量的链接映射列表。其中每个链接映射列表都提供一个唯一的名称空间。rtld-audit 接口使用自己的用于维护审计库的链接映射列表。因此,在应用程序的符号绑定要求中,不涉及审计库。针对每个 rtld-audit 支持库会指定一个唯一的新链接映射标识符。
审计库可以使用 dlmopen(3C) 检查应用程序链接映射列表。将 dlmopen() 与 RTLD_NOLOAD 标志结合使用时,审计库可以在不装入目标文件的情况下查询该目标文件是否存在。
审计库的生成方式与其他任何共享目标文件的生成方式相同。但是,必须注意进程内审计库名称空间的唯一性。
该库必须提供所有依赖性需求。
该库不应使用无法用于进程内多个接口实例的系统接口。
如果审计库引用外部接口,则审计库必须定义提供接口定义的依赖性。例如,如果审计库调用 printf(3C),则审计库必须定义与 libc 之间的依赖性。请参见生成共享目标文件输出文件。由于审计库具有唯一的名称空间,因此,所审计的应用程序中提供的 libc 无法满足符号引用。如果审计库依赖于 libc,则会向进程中装入两种版本的 libc.so.1。一种版本用于满足应用程序链接映射列表的绑定要求,另一种版本用于满足审计链接映射列表的绑定要求。
要确保生成的审计库会记录所有的依赖项,请使用链接编辑器的 -z defs 选项。
部分系统接口会假定其是进程内实现的唯一实例,例如信号和 malloc(3C)。审计库应该避免使用此类接口,因为这样做可能会无意中更改应用程序的行为。
rtld-audit 接口可通过以下两种方法之一来启用。每种方法都会指示一个所审计的目标文件的作用域。
通过在生成目标文件时定义一个或多个审计程序来启用局部审计。请参见记录局部审计程序。通过此方法在运行时可用的审计库附带有与请求局部审计的动态目标文件相关的信息。
通过使用环境变量 LD_AUDIT 定义一个或多个审计程序来启用全局审计。通过将局部审计定义与 -z globalaudit 选项组合,也可以为应用程序启用全局审计。请参见记录全局审计程序。通过此方法在运行时可用的审计库附带有与应用程序所用的所有动态目标文件相关的信息。
这两种定义审计程序的方法均使用一个字符串,其中包含通过 dlmopen(3C) 装入的以冒号分隔的共享目标文件列表。每个目标文件都装入各自的审计链接映射列表中。使用 dlsym(3C) 可搜索每个目标文件中的审计例程。在应用程序执行过程中的不同阶段会调用找到的审计例程。
安全应用程序只能从可信目录中获取审计库。缺省情况下,用于 32 位目标文件的运行时链接程序可识别的可信目录仅有 /lib/secure 和 /usr/lib/secure。对于 64 位目标文件,可信目录是 /lib/secure/64 和 /usr/lib/secure/64。
使用链接编辑器选项 -p 或 -P 生成目标文件时,可以确定局部审计要求。例如,要使用审计库 audit.so.1 审计 libfoo.so.1,请在链接编辑时使用 -p 选项记录要求:
$ cc -G -o libfoo.so.1 -Wl,-paudit.so.1 -K pic foo.c $ elfdump -d libfoo.so.1 | grep AUDIT [2] AUDIT 0x96 audit.so.1
在运行时,如果存在此审计标识符,则会装入审计库。然后,将与标识目标文件相关的信息传递到此审计库。
如果单独使用此机制,则在装入审计库之前会显示搜索标识目标文件之类的信息。要提供尽可能多的审计信息,需要将存在的要求局部审计的目标文件传播给此目标文件的用户。例如,如果生成的应用程序依赖于 libfoo.so.1,则会对此应用程序进行标识,指明其依赖项需要审计:
$ cc -o main main.c libfoo.so.1 $ elfdump -d main | grep AUDIT [4] DEPAUDIT 0x1be audit.so.1
通过此机制启用的审计会导致向审计库中传递与所有应用程序显式依赖项有关的信息。使用链接编辑器的 -P 选项,还可以在创建目标文件时直接记录此依赖项审计:
$ cc -o main main.c -Wl,-Paudit.so.1 $ elfdump -d main | grep AUDIT [3] DEPAUDIT 0x1b2 audit.so.1
通过设置环境变量 LD_AUDIT 可以确定全局审计要求。例如,针对审计库 audit.so.1,此环境变量可用于审计应用程序 main 以及该应用程序的所有依赖项。
$ LD_AUDIT=audit.so.1 main
通过记录应用程序中的局部审计程序以及 -z globalaudit 选项,还可以实现全局审计。例如,通过使用链接编辑器的 -P 选项和 -z globalaudit 选项,可以生成应用程序 main 以启用全局审计。
$ cc -o main main.c -Wl,-Paudit.so.1 -z globalaudit $ elfdump -d main | grep AUDIT [3] DEPAUDIT 0x1b2 audit.so.1 [26] FLAGS_1 0x1000000 [ GLOBAL-AUDITING ]
通过以上任一机制启用的审计会导致向审计库中传递与应用程序的所有动态目标文件有关的信息。
为审计例程提供了一个或多个 cookie。cookie 是描述单个动态目标文件的数据项。最初装入动态目标文件时,为 la_objopen() 例程提供了一个初始 cookie。此 cookie 是指向装入的动态目标文件的关联 Link_map 的指针。但是,la_objopen() 例程可自由分配,并返回至运行时链接程序(备用 cookie)。此机制为审计程序提供了一种使用每个动态目标文件维护其自身数据并使用所有后续审计例程调用接收此数据的方法。
通过 rtld-audit 接口可以提供多个审计库。在这种情况下,从一个审计程序返回的信息将传递至下一个审计程序的同一审计例程。同样,由一个审计程序建立的 cookie 将传递至下一个审计程序。设计预期与其他审计库共存的审计库时应谨慎。一种安全的方法是不应更改通常由运行时链接程序返回的绑定或 cookie。更改这些数据会在后面的审计库中产生意外结果。否则,应设计所有审计程序相互协作以安全更改任何绑定信息或 cookie 信息。
rtld-audit 接口提供了以下例程。这些例程按照其预期的使用顺序进行说明。
此例程可提供运行时链接程序与审计库之间的初次握手。必须提供此接口才能装入审计库。
uint_t la_version(uint_t version);
运行时链接程序通过其可以支持的最高 version 的 rtld-audit 接口来调用此接口。审计库可以检验此版本是否足以供其使用,并返回审计库预期使用的版本。此版本通常为 /usr/include/link.h 中定义的 LAV_CURRENT。
如果审计库返回零,或者返回的版本高于运行时链接程序所支持的 rtld-audit 接口的版本,则会废弃该审计库。
为其余审计例程提供了一个或多个 cookie。请参见审计接口交互。
在 la_version() 调用之后,对 la_objopen() 例程进行了两次调用。第一次调用提供动态可执行文件的链接映射信息,第二次调用提供运行时链接程序的链接映射信息。
uint_t la_objopen(Link_map *lmp, Lmid_t lmid, uintptr_t *cookie);
lmp 提供说明新目标文件的链接映射结构。lmid 标识添加了目标文件的链接映射列表。cookie 提供指向某个标识符的指针。此标识符会初始化为目标文件 lmp。审计库可以重新指定此标识符,以便更好地标识其他 rtld-audit 接口例程的目标文件。
la_objopen() 例程会返回表示与此目标文件相关的符号绑定的值。返回值是 /usr/include/link.h 中定义的以下值的掩码:
LA_FLG_BINDTO-审计到此目标文件的符号绑定。
LA_FLG_BINDFROM-审计来自此目标文件的符号绑定。
通过这些值,审计程序可以选择要使用 la_symbind() 监视的目标文件。返回值为零表示绑定信息与此目标文件无关。
例如,审计程序可以监视从 libfoo.so 到 libbar.so 的绑定。将 la_objopen() 用于 libfoo.so 会返回 LA_FLG_BINDFROM。将 la_objopen() 用于 libbar.so 会返回 LA_FLG_BINDTO。
审计程序可以监视 libfoo.so 与 libbar.so 之间的所有绑定。将 la_objopen() 用于这两个目标文件会返回 LA_FLG_BINDFROM 和 LA_FLG_BINDTO。
审计程序可以监视到 libbar.so 的所有绑定。将 la_objopen() 用于 libbar.so 会返回 LA_FLG_BINDTO。所有 la_objopen() 调用都会返回 LA_FLG_BINDFROM。
使用审计版本 LAV_VERSION5,将一个表示动态可执行文件的 la_objopen() 调用提供给局部审计程序。在这种情况下,审计程序不会返回符号绑定标志,因为它可能装入过晚,而无法监视与该动态可执行文件关联的符号绑定。忽略审计程序返回的任何标志。la_objopen() 调用为局部审计程序提供了一个初始 cookie,它是任意后续 la_preinit() 或 la_activity() 调用所需的。
void la_activity(uintptr_t *cookie, uint_t flags);
cookie 标识作为链接映射标题的目标文件。flags 表示活动类型,如 /usr/include/link.h 中所定义:
LA_ACT_ADD-正在向链接映射列表中添加目标文件。
LA_ACT_DELETE-正在从链接映射列表中删除目标文件。
LA_ACT_CONSISTENT-已经完成目标文件活动。
在动态可执行文件和运行时链接程序进行 la_objopen() 调用之后,在进程启动时调用 LA_ACT_ADD 活动,以指示添加了新的依赖项。对于延迟装入和 dlopen(3C) 事件,也会调用此活动。使用 dlclose(3C) 删除目标文件时,也会调用 LA_ACT_DELETE 活动。
LA_ACT_ADD 和 LA_ACT_DELETE 活动均为预期随后发生的事件的提示。在一些情况下,呈现的事件可能有所不同。例如,如果目标文件未能完全重定位,则添加新目标文件可能会导致一些新目标文件被删除。如果 .fini 执行导致延迟装入新目标文件,则删除目标文件还会导致添加新目标文件。LA_ACT_CONSISTENT 活动在任何目标文件添加或删除操作之后发生,可依赖它来指示应用程序链接映射列表的一致性。审计程序应谨慎检验实际结果,而不是盲目相信 LA_ACT_ADD 和 LA_ACT_DELETE 提示。
对于审计版本 LAV_VERSION1 至 LAV_VERSION4,仅针对全局审计程序调用 la_activity()。对于审计版本 LAV_VERSION5,局部审计程序可获取活动事件。活动事件提供一个表示应用程序链接映射的 cookie。要准备此活动并允许审计程序控制此 cookie 的内容,请首先对局部审计程序进行 la_objopen() 调用。la_objopen() 调用提供一个表示应用程序链接映射的初始 cookie。请参见审计接口交互。
char *la_objsearch(const char *name, uintptr_t *cookie, uint_t flags);
name 表示所搜索的文件名或路径名。cookie 标识启动搜索的目标文件。flags 标识 name 的来源和创建方式,如 /usr/include/link.h 中所定义:
LA_SER_ORIG-初始搜索名称。通常,此名称表示记录为 DT_NEEDED 项的文件名或者提供给 dlopen(3C) 的参数。
LA_SER_RUNPATH-已经通过运行路径组件创建了路径名。
LA_SER_DEFAULT-已经通过缺省搜索路径组件创建了路径名。
LA_SER_CONFIG-路径组件源自配置文件。请参见 crle(1)。
LA_SER_SECURE-路径组件特定于安全目标文件。
返回值会指明运行时链接程序应该继续处理的搜索路径名。值为零表示应该忽略此路径。监视搜索路径的审计库会返回 name。
此例程在过滤器装入新的 filtee 时调用。请参见作为过滤器的共享目标文件。
int la_objfilter(uintptr_t *fltrcook, const char *fltestr, uintptr_t *fltecook, uint_t flags);
fltrcook 标识过滤器。fltestr 指向 filtee 字符串。fltecook 标识 filtee。flags 当前未使用。对于过滤器和 filtee,la_objfilter() 在 la_objopen() 之后调用。
值为零表示应该忽略此 filtee。监视过滤器使用情况的审计库会返回非零值。
为应用程序装入所有目标文件之后但在将控制权转交给应用程序之前,会调用一次此例程。
void la_preinit(uintptr_t *cookie);
cookie 标识启动进程的主目标文件,通常为动态可执行文件。
对于审计版本 LAV_VERSION1 至 LAV_VERSION4,仅针对全局审计程序调用 la_preinit()。对于审计版本 LAV_VERSION5,局部审计程序可获取 preinit 事件。preinit 事件提供一个表示应用程序链接映射的 cookie。要准备此 preinit 并允许审计程序控制此 cookie 的内容,请首先对局部审计程序进行 la_objopen() 调用。la_objopen() 调用提供一个表示应用程序链接映射的初始 cookie。请参见审计接口交互。
在已经通过 la_objopen() 标记用于绑定通知的两个目标文件之间进行绑定时,会调用此例程。
uintptr_t la_symbind32(Elf32_Sym *sym, uint_t ndx, uintptr_t *refcook, uintptr_t *defcook, uint_t *flags); uintptr_t la_symbind64(Elf64_Sym *sym, uint_t ndx, uintptr_t *refcook, uintptr_t *defcook, uint_t *flags, const char *sym_name);
sym 是构造的符号结构,其 sym->st_value 表示所绑定的符号定义的地址。请参见 /usr/include/sys/elf.h。la_symbind32() 可将 sym->st_name 调整为指向实际符号名称。la_symbind64() 可保留 sym->st_name 作为绑定目标文件字符串表的索引。
ndx 表示绑定目标文件的动态符号表内的符号索引。refcook 标识引用此符号的目标文件。此标识符与传递给 la_objopen() 例程的标识符相同,此例程会返回 LA_FLG_BINDFROM。defcook 标识定义此符号的目标文件。此标识符与传递给 la_objopen() 的标识符相同,此函数会返回 LA_FLG_BINDTO。
flags 指向可以传送与绑定相关的信息的数据项。此数据项还可用于修改对此过程链接表项的继续审计。该值是 /usr/include/link.h 中定义的符号绑定标志的掩码。
可以为 la_symbind() 提供以下标志:
LA_SYMB_DLSYM-由于调用 dlsym(3C) 而发生的符号绑定。
LA_SYMB_ALTVALUE (LAV_VERSION2)-通过先前调用 la_symbind() 为符号值返回替换值。
如果 la_pltenter() 或 la_pltexit() 例程存在,则对于过程链接表的各项,这些例程在 la_symbind() 之后调用。每次引用符号时都会调用这些例程。另请参见审计接口限制。
la_symbind() 可以提供以下标志来更改此缺省行为。这些标志可应用于按位或运算(包含边界值),并通过 flags 参数来指示其值。
LA_SYMB_NOPLTENTER-请勿针对此符号调用 la_pltenter() 例程。
LA_SYMB_NOPLTEXIT-请勿针对此符号调用 la_pltexit() 例程。
返回值表示在此调用后将控制权传递到的地址。监视符号绑定的审计库应该返回值 sym->st_value,以便将控制权传递给绑定符号定义。审计库可以通过返回不同的值对符号绑定进行专门重定向。
sym_name 仅适用于 la_symbind64(),其中包含所处理的符号的名称。对于 32 位接口,可在 sym->st_name 字段中使用此名称。
这些例程是系统特定的。调用过程链接表中位于已经标记用于绑定通知的两个目标文件之间的一项时,会调用这些例程。
uintptr_t la_sparcv8_pltenter(Elf32_Sym *sym, uint_t ndx, uintptr_t *refcook, uintptr_t *defcook, La_sparcv8_regs *regs, uint_t *flags); uintptr_t la_sparcv9_pltenter(Elf64_Sym *sym, uint_t ndx, uintptr_t *refcook, uintptr_t *defcook, La_sparcv9_regs *regs, uint_t *flags, const char *sym_name); uintptr_t la_i86_pltenter(Elf32_Sym *sym, uint_t ndx, uintptr_t *refcook, uintptr_t *defcook, La_i86_regs *regs, uint_t *flags); uintptr_t la_amd64_pltenter(Elf64_Sym *sym, uint_t ndx, uintptr_t *refcook, uintptr_t *defcook, La_amd64_regs *regs, uint_t *flags, const char *sym_name);
sym、ndx、refcook、defcook 和 sym_name 提供的信息与传递给 la_symbind() 的信息相同。
对于 la_sparcv8_pltenter() 和 la_sparcv9_pltenter(),regs 指向输出寄存器。对于 la_i86_pltenter(),regs 指向栈寄存器和帧寄存器。对于 la_amd64_pltenter(),regs 指向栈寄存器、帧寄存器以及用于传递整数参数的寄存器。regs 在 /usr/include/link.h 中定义。
flags 指向可以传送与绑定相关的信息的数据项。此数据项可用于修改对此过程链接表项的继续审计。此数据项与 la_symbind() 中的 flags 指向的数据项相同。
la_pltenter() 可以提供以下标志来更改当前的审计行为。这些标志可应用于按位或运算(包含边界值),并通过 flags 参数来指示其值。
LA_SYMB_NOPLTENTER-不能针对此符号再次调用 la_pltenter()。
LA_SYMB_NOPLTEXIT-不能针对此符号再次调用 la_pltexit()。
返回值表示在此调用后将控制权传递到的地址。监视符号绑定的审计库应该返回值 sym->st_value,以便将控制权传递给绑定符号定义。审计库可以通过返回不同的值对符号绑定进行专门重定向。
返回过程链接表中位于已经标记用于绑定通知的两个目标文件之间的一项时,会调用此例程。此例程在调用者获取控制权之前调用。
uintptr_t la_pltexit(Elf32_Sym *sym, uint_t ndx, uintptr_t *refcook, uintptr_t *defcook, uintptr_t retval); uintptr_t la_pltexit64(Elf64_Sym *sym, uint_t ndx, uintptr_t *refcook, uintptr_t *defcook, uintptr_t retval, const char *sym_name);
sym、ndx、refcook、defcook 和 sym_name 提供的信息与传递给 la_symbind() 的信息相同。retval 是绑定函数的返回代码。监视符号绑定的审计库应该返回 retval。审计库可以专门返回不同的值。
此例程在执行目标文件的任何终止代码之后和卸载目标文件之前调用。
uint_t la_objclose(uintptr_t *cookie);
cookie 标识目标文件,并从先前的 la_objopen() 中获取。当前会忽略任何返回值。
以下简单示例创建了一个审计库,其中显示了动态可执行文件 date(1) 装入的每个共享目标文件依赖项的名称。
$ cat audit.c #include <link.h> #include <stdio.h> uint_t la_version(uint_t version) { return (LAV_CURRENT); } uint_t la_objopen(Link_map *lmp, Lmid_t lmid, uintptr_t *cookie) { if (lmid == LM_ID_BASE) (void) printf("file: %s loaded\n", lmp->l_name); return (0); } $ cc -o audit.so.1 -G -K pic -z defs audit.c -lmapmalloc -lc $ LD_AUDIT=./audit.so.1 date file: date loaded file: /lib/libc.so.1 loaded file: /lib/libm.so.2 loaded file: /usr/lib/locale/en_US/en_US.so.2 loaded Thur Aug 10 17:03:55 PST 2000
/usr/demo/link_audit 下的 SUNWosdem 软件包中提供了许多使用 rtld-audit 接口的演示应用程序。
sotruss(1) 和 whocalls(1) 包括在 SUNWtoo 软件包中。perfcnt 和 symbindrep 是程序示例。这些应用程序不适用于生产环境。
rtld-audit 实现中存在一些限制。设计审计库时应谨慎了解这些限制。
将目标文件添加到进程时审计库会接收到信息。审计库接收到这种信息时,所监视目标文件可能无法执行。例如,对于装入的目标文件,审计程序可能会接收到 la_objopen() 调用。但是,在该目标文件中的任何代码可以使用之前,该目标文件必须装入其自身的依赖项并进行重定位。审计库可能需要通过使用 dlopen(3C) 获取句柄来检查装入的目标文件。然后,通过使用 dlsym(3C),该句柄可用于搜索接口。但是,除非已知目标文件的初始化已完成,否则不应调用采用这种方式获取的接口。
使用 la_pltexit() 系列存在一些限制。这些限制是由于需要在调用者和被调用者之间插入额外栈帧,以提供 la_pltexit() 返回值。此要求在仅调用 la_pltenter() 例程时不会产生问题。在这种情况下,可以在将控制权转交给目标函数之前清除任何干预栈。
由于存在这些限制,因此应该将 la_pltexit() 视为实验接口。在不确定的情况下,请避免使用 la_pltexit() 例程。
有少量函数可以直接检查栈或对其状态做出假设。这些函数的一些示例包括 setjmp(3C) 系列、vfork(2) 以及返回结构而不是指向结构的指针的任何函数。为支持 la_pltexit() 而创建的额外栈会破坏这些函数。
由于运行时链接程序无法检测此类型的函数,因此,审计库创建者会负责针对此类例程禁用 la_pltexit()。