通过soname替换共享对象的插入部分

8
我编写了一个共享对象,通过 LD_PRELOADdlsym 与 FreeType 的 FT_Load_GlyphFT_Render_Glyph 函数进行交互以修改参数。
这个方法很好用,但我想知道是否有一种方法可以实现以下更改:
  • 对于在特定主机(例如 Debian)上使用 FreeType 的所有程序;
  • 不破坏任何未实际链接到 FreeType 的程序;
  • 不仅仅是对主机上的所有程序应用 LD_PRELOAD
  • 无需进行任何维护,除非 FreeType 的 soname 改变;
  • 不修改 FreeType 文件或主机上任何程序的文件。
我能想到的唯一两个“解决方案”都是丑陋的 hack:
  • 始终对所有程序应用 LD_PRELOAD,这似乎很慢且容易出错;或者
  • 将例如 libfreetype.so.6.12.3 复制到 libxxxxtype.so.6.12.3,然后
    • libxxxxtype.so.6.12.3 中修补 soname 为 libxxxxtype.so.6
    • 将交互共享对象链接到 libxxxxtype.so.6;并且
    • 将共享对象安装为例如 libfreetype.so.6.999
我希望在不一定能够访问共享对象或使用它的程序的源代码的情况下,透明地修补共享对象中的一些函数,同时让其余函数通过。但是,如果我创建一个名为 libfreetype.so.6 的假共享对象,我无法找到一种干净的方式将其链接到(或者说 dlopen)真正的 libfreetype.so.6
这是我第一次尝试使用共享库,请谅解如果这个问题有一些错误的假设或根本没有意义。

1
基于重命名 libfreetype.so.x.y.z 的解决方案似乎是正确的做法。为什么你要将其描述为丑陋的呢? - Leon
我认为我的原因是(a)我需要维护一个真正的libfreetype.so的副本,该副本具有修补后的soname,特别是在安装新版本(或相同版本)的libfreetype6软件包时保持其最新,并且(b)我将会污染全局的“sonamespace” ,其中至少从理论上讲是脆弱的,因为不可能想出绝对没有任何库作者会使用的名称来。 (a)可以通过glorpen的答案(换取依赖于绝对路径)得到缓解,而(b)可以得到缓解,使其仅在理论上存在。 - Delan Azabani
3个回答

3

你可以尝试使用uprobes从某些函数中动态窃取控制权吗?

请查看http://www.brendangregg.com/blog/2015-06-28/linux-ftrace-uprobe.html

uprobes是用户级别的动态跟踪,在Linux 3.5中添加,且在Linux 3.14中得到改进。它允许您跟踪用户级别的函数;例如,来自所有运行的bash shell的readline()函数的返回值,以及返回的字符串:

# ./uprobe 'r:bash:readline +0($retval):string'
Tracing uprobe readline (r:readline /bin/bash:0x8db60 +0($retval):string). Ctrl-C to end.
 bash-11886 [003] d... 19601837.001935: readline: (0x41e876 <- 0x48db60) arg1="ls -l"
 bash-11886 [002] d... 19601851.008409: readline: (0x41e876 <- 0x48db60) arg1="echo "hello world""
 bash-11886 [002] d... 19601854.099730: readline: (0x41e876 <- 0x48db60) arg1="df -h"
 bash-11886 [002] d... 19601858.805740: readline: (0x41e876 <- 0x48db60) arg1="cd .."
 bash-11886 [003] d... 19601898.378753: readline: (0x41e876 <- 0x48db60) arg1="foo bar"
^C
Ending tracing...

并且 http://www.brendangregg.com/blog/2015-07-03/hacking-linux-usdt-ftrace.html

还有其他跟踪用户空间函数的解决方案,例如ftrace、systemtap、dtrace、lttng等。其中一些需要重新编译并在程序中静态定义跟踪点;uprobes是“用户级动态跟踪”。

关于uprobes的一些链接:

uprobes有一个名为handler的函数,其中包含pt_regs。正如最后一个链接中所述:“uprobes因此实现了一种机制,可以在进程执行特定指令位置时调用内核函数。”,并且它表明uprobes可能取代一些基于ptrace/gdb的解决方案;因此,有可能通过更改eip/rip(PC)寄存器来改变命中活动uprobes的任何程序的执行。

您可以尝试其他动态插装工具,如pindyninst; 但它们是为每个进程使用而设计的。


uprobes看起来非常有趣 - 我会试一试!这个问题的所有三个答案都很到位,但我会给你赏金,因为你的技术不仅与ld.so无关,而且是我从未听说过的技术。 - Delan Azabani
我不确定如何使用uprobes来实现interposing;但是Gregg在这里http://stackoverflow.com/users/2603561/brendan-gregg,他可能有一些想法。@Brendan Gregg,你觉得呢? - osgx
我看到很多人在Stack Overflow上@其他用户。这样做(或者写一个链接到他们的个人资料)会在通知方面有任何作用吗? - Delan Azabani
http://meta.stackexchange.com/questions/43019/how-do-comment-replies-work(通过互联网搜索-stackoverflow.com通知用户)。但是我的评论没有生成通知;它们通常限于在页面上的用户(回答或评论)。您可以通知他。 - osgx
Delan,刚开始在uprobes上进行实验:我能够在Ubuntu 16.04上uprobe libc.so函数(readdir),并从使用[perf probe](http://linux.die.net/man/1/perf-probe)/trace-cmd的应用程序中获取它们的使用信息。文档说https://www.redhat.com/archives/utrace-devel/2009-June/msg00024.html“探针处理程序可以修改被探测函数的环境——例如,通过修改数据结构或修改pt_regs结构的内容……因此,Uprobes可以用于安装错误修复或注入故障以进行测试。” stap(systemtap)也可能有所帮助。 - osgx

2
另一种解决方案是创建系统范围的库“覆盖层”,使用自定义的libfreetype,并将未修改的方法代理到真实的lib。
您必须使自定义库与真实库兼容。您可以使用绝对路径(例如dlopen("/usr/lib64/libfreetype.so.6"))使用dlopen复制实际输出的函数的定义,并使用dlsym代理它们。 为了更方便地进行维护,您甚至可以将代理的参数类型替换为简单的void*。 只有在freetype函数更改时(参数计数,函数名称),您才需要进行更改。
要创建库“覆盖层”,您可以将自定义库安装到例如“/opt/myapp/lib64/libfreetype.so.6”中,然后将此路径添加到动态链接器运行时路径中。您可能需要为其他版本创建符号链接或编译新的自定义库,如果原始实现发生更改,则需要进行更改才能遮蔽真实库并使其他应用程序正常工作:)
谷歌表示,在Debian上更改运行时加载路径只需编辑/etc/ld.so.conf即可。在开头添加/opt/myapp/lib64路径,以便首先检查它。 现在,任何搜索freetype的应用程序都应该加载您的库,您可以使用ldd

好的!我以为我已经尝试过了,但是当使用相同的soname打开库时,dlopen(3)什么也没做,但是我已经六周没有碰这个项目了,所以我可能是在胡说八道。如果dlopen(3)提供了一种按soname“从解析顺序中的下一个路径到最后一个路径”打开库的方法,那就太好了,这样我就不必使用绝对路径了,但到那时,我将会用尽各种方法来批评你的方法。 - Delan Azabani

2
要在所有程序中LD_PRELOAD,这似乎很慢且不稳定。那是个好的解决方案(为你想要的)。我看不出更好的解决方案。
它并不脆弱。它以一种被记录的方式向运行时链接器提供信息。您没有敲打任何东西,假装某些东西不是它所代表的。您只是改变了函数名称解析的优先级层次结构。
它不慢。链接器必须有时候做一些事情。它必须检查是否定义了LD_PRELOAD,而这在任何情况下都是一个用户空间操作。因此,它将沿着那条路径前进,在执行大量其他工作之前加载您的库。在正常情况下,我会感到惊讶,如果时间甚至可以测量。
我有两个问题,但它们与技术无关。代码实际上必须在所有情况下起作用,并且您必须深入研究进程创建框架,以确保LD_PRELOAD真正在每个地方定义。除此之外,ld.so精确定义其环境变量,以符合您的预期使用。谁能反驳呢?

你是对的 - 我很高兴用“似乎”搪塞过去,因为我没有时间对加载库所需的时间进行基准测试以获得具体的想法。我唯一的顾虑是我可能会污染全局命名空间(如“哦不,现在每个程序都有一个FT_Load_Glyph!”),但我认为只有在程序在正确运行时依赖于FT_Load_Glyph的不存在或依赖于与FreeType无关的FT_Load_Glyph的情况下才会有影响 - 这两种情况都似乎非常牵强。 - Delan Azabani
就你对这种方法的担忧,我一直在使用/etc/ld.so.preload来避免查找如何配置PAM(或systemd或其他)以全局定义环境变量,尽管我相当确定那些方式并不比/etc/ld.so.preload更“标准”(从POSIX的角度来看),而且/etc/ld.so.preload似乎是一个特定于glibc的功能。你所说的确保代码“在所有情况下都能工作”的意思是什么? - Delan Azabani

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接