动态连接的诀窍：使用 LD_PRELOAD 去欺骗、注入特性和研究程序
本文假设你具备基本的 C 技能Linux 完全在你的控制之中。虽然从每个人的角度来看似乎并不总是这样，但是高级用户喜欢去控制它。我将向你展示一个基本的诀窍，在很大程度上你可以去影响大多数程序的行为，它并不仅是好玩，在有时候也很有用。一个让我们产生兴趣的示例让我们以一个简单的示例开始。先乐趣，后科学。random\_num.c：#include <stdio.h> #include <stdlib.h> #include <time.h> int main(){ srand(time(NULL)); int i = 10; while(i--) printf("%d\n",rand()%100); return 0; } 我相信，它足够简单吧。我不使用任何参数来编译它，如下所示：gcc random_num.c -o random_num 我希望它输出的结果是明确的：从 0-99 中选择的十个随机数字，希望每次你运行这个程序时它的输出都不相同。现在，让我们假装真的不知道这个可执行程序的出处。甚至将它的源文件删除，或者把它移动到别的地方 —— 我们已不再需要它了。我们将对这个程序的行为进行重大的修改，而你并不需要接触到它的源代码，也不需要重新编译它。因此，让我们来创建另外一个简单的 C 文件：unrandom.c：int rand(){ return 42; //the most random number in the universe } 我们将编译它进入一个共享库中。gcc -shared -fPIC unrandom.c -o unrandom.so 因此，现在我们已经有了一个可以输出一些随机数的应用程序，和一个定制的库，它使用一个常数值 42 实现了一个 rand() 函数。现在 …… 就像运行 random_num 一样，然后再观察结果：LD_PRELOAD=$PWD/unrandom.so ./random_nums 如果你想偷懒或者不想自动亲自动手（或者不知什么原因猜不出发生了什么），我来告诉你 —— 它输出了十次常数 42。如果先这样执行export LD_PRELOAD=$PWD/unrandom.so 然后再以正常方式运行这个程序，这个结果也许会更让你吃惊：一个未被改变过的应用程序在一个正常的运行方式中，看上去受到了我们做的一个极小的库的影响 ……等等，什么？刚刚发生了什么？是的，你说对了，我们的程序生成随机数失败了，因为它并没有使用 “真正的” rand()，而是使用了我们提供的的那个 —— 它每次都返回 42。但是，我们告诉过它去使用真实的那个。我们编程让它去使用真实的那个。另外，在创建那个程序的时候，假冒的 rand() 甚至并不存在！这句话并不完全正确。我们只能告诉它去使用 rand()，但是我们不能去选择哪个 rand() 是我们希望我们的程序去使用的。当我们的程序启动后，（为程序提供所需要的函数的）某些库被加载。我们可以使用 ldd 去学习它是怎么工作的：$ ldd random_nums linux-vdso.so.1 => (0x00007fff4bdfe000) libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f48c03ec000) /lib64/ld-linux-x86-64.so.2 (0x00007f48c07e3000) 正如你看到的输出那样，它列出了被程序 random_nums 所需要的库的列表。这个列表是构建进可执行程序中的，并且它是在编译时决定的。在你的机器上的具体的输出可能与示例有所不同，但是，一个 libc.so 肯定是有的 —— 这个文件提供了核心的 C 函数。它包含了 “真正的” rand()。我使用下列的命令可以得到一个全部的函数列表，我们看一看 libc 提供了哪些函数：nm -D /lib/libc.so.6 这个 nm 命令列出了在一个二进制文件中找到的符号。-D 标志告诉它去查找动态符号，因为 libc.so.6 是一个动态库。这个输出是很长的，但它确实在列出的很多标准函数中包括了 rand()。现在，在我们设置了环境变量 LD_PRELOAD 后发生了什么？这个变量 为一个程序强制加载一些库。在我们的案例中，它为 random_num 加载了 unrandom.so，尽管程序本身并没有这样去要求它。下列的命令可以看得出来：$ LD_PRELOAD=$PWD/unrandom.so ldd random_nums linux-vdso.so.1 => (0x00007fff369dc000) /some/path/to/unrandom.so (0x00007f262b439000) libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f262b044000) /lib64/ld-linux-x86-64.so.2 (0x00007f262b63d000) 注意，它列出了我们当前的库。实际上这就是代码为什么得以运行的原因：random_num 调用了 rand()，但是，如果 unrandom.so 被加载，它调用的是我们所提供的实现了 rand() 的库。很清楚吧，不是吗？更清楚地了解这还不够。我可以用相似的方式注入一些代码到一个应用程序中，并且用这种方式它能够像个正常的函数一样工作。如果我们使用一个简单的 return 0 去实现 open() 你就明白了。我们看到这个应用程序就像发生了故障一样。这是 显而易见的， 真实地去调用原始的 open()：inspect\_open.c：int open(const char *pathname, int flags){ /* Some evil injected code goes here. */ return open(pathname,flags); // Here we call the "real" open function, that is provided to us by libc.so } 嗯，不对。这将不会去调用 “原始的” open(...)。显然，这是一个无休止的递归调用。怎么去访问这个 “真正的” open() 函数呢？它需要去使用程序接口进行动态链接。它比听起来更简单。我们来看一个完整的示例，然后，我将详细解释到底发生了什么：inspect\_open.c：#define _GNU_SOURCE #include <dlfcn.h> typedef int (*orig_open_f_type)(const char *pathname, int flags); int open(const char *pathname, int flags, ...) { /* Some evil injected code goes here. */ orig_open_f_type orig_open; orig_open = (orig_open_f_type)dlsym(RTLD_NEXT,"open"); return orig_open(pathname,flags); } dlfcn.h 是我们后面用到的 dlsym 函数所需要的。那个奇怪的 #define 是命令编译器去允许一些非标准的东西，我们需要它来启用 dlfcn.h 中的 RTLD_NEXT。那个 typedef 只是创建了一个函数指针类型的别名，它的参数等同于原始的 open —— 它现在的别名是 orig_open_f_type，我们将在后面用到它。我们定制的 open(...) 的主体是由一些代码构成。它的最后部分创建了一个新的函数指针 orig_open，它指向原始的 open(...) 函数。为了得到那个函数的地址，我们请求 dlsym 在动态库堆栈上为我们查找下一个 open() 函数。最后，我们调用了那个函数（传递了与我们的假冒 open() 一样的参数），并且返回它的返回值。我使用下面的内容作为我的 “邪恶的注入代码”：inspect\_open.c (片段)：printf("The victim used open(...) to access '%s'!!!\n",pathname); //remember to include stdio.h! 要编译它，我需要稍微调整一下编译参数：gcc -shared -fPIC inspect_open.c -o inspect_open.so -ldl 我增加了 -ldl，因此，它将这个共享库链接到 libdl —— 它提供了 dlsym 函数。（不，我还没有创建一个假冒版的 dlsym ，虽然这样更有趣）因此，结果是什么呢？一个实现了 open(...) 函数的共享库，除了它有 输出 文件路径的意外作用以外，其它的表现和真正的 open(...) 函数 一模一样。:-)如果这个强大的诀窍还没有说服你，是时候去尝试下面的这个示例了：LD_PRELOAD=$PWD/inspect_open.so gnome-calculator 我鼓励你去看看自己实验的结果，但是简单来说，它实时列出了这个应用程序可以访问到的每个文件。我相信它并不难想像为什么这可以用于去调试或者研究未知的应用程序。请注意，这个特定诀窍并不完整，因为 open() 并不是唯一一个打开文件的函数 …… 例如，在标准库中也有一个 open64()，并且为了完整地研究，你也需要为它去创建一个假冒的。可能的用法如果你一直跟着我享受上面的过程，让我推荐一个使用这个诀窍能做什么的一大堆创意。记住，你可以在不损害原始应用程序的同时做任何你想做的事情！获得 root 权限。你想多了！你不会通过这种方法绕过安全机制的。（一个专业的解释是：如果 ruid != euid，库不会通过这种方法预加载的。）欺骗游戏：取消随机化。这是我演示的第一个示例。对于一个完整的工作案例，你将需要去实现一个定制的 random() 、rand_r()、random_r()，也有一些应用程序是从 /dev/urandom 之类的读取，你可以通过使用一个修改过的文件路径来运行原始的 open() 来把它们重定向到 /dev/null。而且，一些应用程序可能有它们自己的随机数生成算法，这种情况下你似乎是没有办法的（除非，按下面的第 10 点去操作）。但是对于一个新手来说，它看起来很容易上手。欺骗游戏：让子弹飞一会 。实现所有的与时间有关的标准函数，让假冒的时间变慢两倍，或者十倍。如果你为时间测量和与时间相关的 sleep 或其它函数正确地计算了新的值，那么受影响的应用程序将认为时间变慢了（你想的话，也可以变快），并且，你可以体验可怕的 “子弹时间” 的动作。或者 甚至更进一步，你的共享库也可以成为一个 DBus 客户端，因此你可以使用它进行实时的通讯。绑定一些快捷方式到定制的命令，并且在你的假冒的时间函数上使用一些额外的计算，让你可以有能力按你的意愿去启用和禁用慢进或快进任何时间。研究应用程序：列出访问的文件。它是我演示的第二个示例，但是这也可以进一步去深化，通过记录和监视所有应用程序的文件 I/O。研究应用程序：监视因特网访问。你可以使用 Wireshark 或者类似软件达到这一目的，但是，使用这个诀窍你可以真实地控制基于 web 的应用程序发送了什么，不仅是看看，而是也能影响到交换的数据。这里有很多的可能性，从检测间谍软件到欺骗多用户游戏，或者分析和逆向工程使用闭源协议的应用程序。研究应用程序：检查 GTK 结构 。为什么只局限于标准库？让我们在所有的 GTK 调用中注入一些代码，因此我们就可以知道一个应用程序使用了哪些组件，并且，知道它们的构成。然后这可以渲染出一个图像或者甚至是一个 gtkbuilder 文件！如果你想去学习一些应用程序是怎么管理其界面的，这个方法超级有用！在沙盒中运行不安全的应用程序。如果你不信任一些应用程序，并且你可能担心它会做一些如 rm -rf / 或者一些其它不希望的文件活动，你可以通过修改传递到文件相关的函数（不仅是 open ，也包括删除目录等）的参数，来重定向所有的文件 I/O 操作到诸如 /tmp 这样地方。还有更难的诀窍，如 chroot，但是它也给你提供更多的控制。它可以更安全地完全 “封装”，但除非你真的知道你在做什么，不要以这种方式真的运行任何恶意软件。实现特性 。zlibc 是明确以这种方法运行的一个真实的库；它可以在访问文件时解压文件，因此，任何应用程序都可以在无需实现解压功能的情况下访问压缩数据。修复 bug。另一个现实中的示例是：不久前（我不确定现在是否仍然如此）Skype（它是闭源的软件）从某些网络摄像头中捕获视频有问题。因为 Skype 并不是自由软件，源文件不能被修改，这就可以通过使用预加载一个解决了这个问题的库的方式来修复这个 bug。手工方式 访问应用程序拥有的内存。请注意，你可以通过这种方式去访问所有应用程序的数据。如果你有类似的软件，如 CheatEngine/scanmem/GameConqueror 这可能并不会让人惊讶，但是，它们都要求 root 权限才能工作，而 LD_PRELOAD 则不需要。事实上，通过一些巧妙的诀窍，你注入的代码可以访问所有的应用程序内存，从本质上看，是因为它是通过应用程序自身得以运行的。你可以修改这个应用程序能修改的任何东西。你可以想像一下，它允许你做许多的底层的侵入…… ，但是，关于这个主题，我将在某个时候写一篇关于它的文章。这里仅是一些我想到的创意。我希望你能找到更多，如果你做到了 —— 通过下面的评论区共享出来吧！via: https://rafalcieslak.wordpress.com/2013/04/02/dynamic-linker-tricks-using-ld_preload-to-cheat-inject-features-and-investigate-programs/作者：Rafał Cieślak 译者：qhwdw 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
你能快速定位CPU性能回退的问题么？ 如果你的工作环境非常复杂且变化快速，那么使用现有的工具是来定位这类问题是很具有挑战性的。当你花掉数周时间把根因找到时，代码已经又变更了好几轮，新的性能问题又冒了出来。幸亏有了CPU火焰图（flame graphs），CPU使用率的问题一般都比较好定位。但要处理性能回退问题，就要在修改前后的火焰图之间，不断切换对比，来找出问题所在，这感觉就是像在太阳系中搜寻冥王星。虽然，这种方法可以解决问题，但我觉得应该会有更好的办法。所以，下面就隆重介绍红/蓝差分火焰图（red/blue differential flame graphs）：上面是一副交互式SVG格式图片（链接）。图中使用了两种颜色来表示状态，红色表示增长，蓝色表示衰减。这张火焰图中各火焰的形状和大小都是和第二次抓取的profile文件对应的CPU火焰图是相同的。（其中，y轴表示栈的深度，x轴表示样本的总数，栈帧的宽度表示了profile文件中该函数出现的比例，最顶层表示正在运行的函数，再往下就是调用它的栈）在下面这个案例展示了，在系统升级后，一个工作载荷的CPU使用率上升了。 下面是对应的CPU火焰图（SVG格式）通常，在标准的火焰图中栈帧和栈塔的颜色是随机选择的。 而在红/蓝差分火焰图中，使用不同的颜色来表示两个profile文件中的差异部分。在第二个profile中deflate\_slow()函数以及它后续调用的函数运行的次数要比前一次更多，所以在上图中这个栈帧被标为了红色。可以看出问题的原因是ZFS的压缩功能被启用了，而在系统升级前这项功能是关闭的。这个例子过于简单，我甚至可以不用差分火焰图也能分析出来。但想象一下，如果是在分析一个微小的性能下降，比如说小于5%，而且代码也更加复杂的时候，问题就为那么好处理了。红/蓝差分火焰图这个事情我已经讨论了好几年了，最终我自己编写了一个我个人认为有价值的实现。它的工作原理是这样的：抓取修改前的堆栈profile1文件抓取修改后的堆栈profile2文件使用profile2来生成火焰图。（这样栈帧的宽度就是以profile2文件为基准的）使用“2 - 1”的差异来对火焰图重新上色。上色的原则是，如果栈帧在profile2中出现出现的次数更多，则标为红色，否则标为蓝色。色彩是根据修改前后的差异来填充的。这样做的目的是，同时使用了修改前后的profile文件进行对比，在进行功能验证测试或者评估代码修改对性能的影响时，会非常有用。新的火焰图是基于修改后的profile文件生成（所以栈帧的宽度仍然显示了当前的CPU消耗），通过颜色的对比，就可以了解到系统性能差异的原因。只有对性能产生直接影响的函数才会标注颜色（比如说，正在运行的函数），它所调用的子函数不会重复标注。生成红/蓝差分火焰图我已经把一个简单的代码实现推送到github上（见火焰图），其中新增了一个程序脚本，difffolded.pl。为了展示工具是如何工作的，用Linux perf\_events 来演示一下操作步骤。（你也可以使用其他profiler）抓取修改前的profile 1文件:# perf record -F 99 -a -g -- sleep 30 # perf script > out.stacks1 一段时间后 (或者程序代码修改后), 抓取profile 2文件:# perf record -F 99 -a -g -- sleep 30 # perf script > out.stacks2 现在将 profile 文件进行折叠（fold）, 再生成差分火焰图:$ git clone --depth 1 http://github.com/brendangregg/FlameGraph $ cd FlameGraph $ ./stackcollapse-perf.pl ../out.stacks1 > out.folded1 $ ./stackcollapse-perf.pl ../out.stacks2 > out.folded2 $ ./difffolded.pl out.folded1 out.folded2 | ./flamegraph.pl > diff2.svg difffolded.p只能对“折叠”过的堆栈profile文件进行操作，折叠操作是由前面的stackcollapse系列脚本完成的。（见链接火焰图）。 脚本共输出3列数据，其中一列代表折叠的调用栈，另两列为修改前后profile文件的统计数据。func_a;func_b;func_c 31 33 [...] 在上面的例子中"funca()->funcb()->func\_c()" 代表调用栈，这个调用栈在profile1文件中共出现了31次，在profile2文件中共出现了33次。然后，使用flamegraph.pl脚本处理这3列数据，会自动生成一张红/蓝差分火焰图。其他选项再介绍一些有用的选项：difffolded.pl -n：这个选项会把两个profile文件中的数据规范化，使其能相互匹配上。如果你不这样做，抓取到所有栈的统计值肯定会不相同，因为抓取的时间和CPU负载都不同。这样的话，看上去要么就是一片红（负载增加），要么就是一片蓝（负载下降）。-n选项对第一个profile文件进行了平衡，这样你就可以得到完整红/蓝图谱。difffolded.pl -x: 这个选项会把16进制的地址删掉。 profiler时常会无法将地址转换为符号，这样的话栈里就会有16进制地址。如果这个地址在两个profile文件中不同，这两个栈就会认为是不同的栈，而实际上它们是相同的。遇到这样的问题就用-x选项搞定。flamegraph.pl --negate: 用于颠倒红/蓝配色。 在下面的章节中，会用到这个功能。不足之处虽然我的红/蓝差分火焰图很有用，但实际上还是有一个问题：如果一个代码执行路径完全消失了，那么在火焰图中就找不到地方来标注蓝色。你只能看到当前的CPU使用情况，而不知道为什么会变成这样。一个办法是，将对比顺序颠倒，画一个相反的差分火焰图。例如：上面的火焰图是以修改前的profile文件为基准，颜色表达了将要发生的情况。右边使用蓝色高亮显示的部分，从中可以看出修改后CPU Idle消耗的CPU时间会变少。（其实，我通常会把cpuidle给过滤掉，使用命令行grep -v cpuidle）图中把消失的代码也突显了出来（或者应该是说，没有突显），因为修改前并没有使能压缩功能，所以它没有出现在修改前的profile文件了，也就没有了被表为红色的部分。下面是对应的命令行：$ ./difffolded.pl out.folded2 out.folded1 | ./flamegraph.pl --negate > diff1.svg 这样，把前面生成diff2.svg一并使用，我们就能得到：diff1.svg: 宽度是以修改前profile文件为基准，颜色表明将要发生的情况diff2.svg: 宽度是以修改后profile文件为基准，颜色表明已经发生的情况如果是在做功能验证测试，我会同时生成这两张图。CPI 火焰图这些脚本开始是被使用在CPI火焰图的分析上。与比较修改前后的profile文件不同，在分析CPI火焰图时，可以分析CPU工作周期与停顿周期的差异变化，这样可以凸显出CPU的工作状态来。其他的差分火焰图也有其他人做过类似的工作。Robert Mustacchi在不久前也做了一些尝试，他使用的方法类似于代码检视时的标色风格：只显示了差异的部分，红色表示新增（上升）的代码路径，蓝色表示删除（下降）的代码路径。一个关键的差别是栈帧的宽度只体现了差异的样本数。右边是一个例子。这个是个很好的主意，但在实际使用中会感觉有点奇怪，因为缺失了完整profile文件的上下文作为背景，这张图显得有些难以理解。Cor-Paul Bezemer也制作了一种差分显示方法flamegraphdiff，他同时将3张火焰图放在同一张图中，修改前后的标准火焰图各一张，下面再补充了一张差分火焰图，但栈帧宽度也是差异的样本数。 上图是一个例子。在差分图中将鼠标移到栈帧上，3张图中同一栈帧都会被高亮显示。这种方法中补充了两张标准的火焰图，因此解决了上下文的问题。我们3人的差分火焰图，都各有所长。三者可以结合起来使用：Cor-Paul方法中上方的两张图，可以用我的diff1.svg 和 diff2.svg。下方的火焰图可以用Robert的方式。为保持一致性，下方的火焰图可以用我的着色方式：蓝->白->红。火焰图正在广泛传播中，现在很多公司都在使用它。如果大家知道有其他的实现差分火焰图的方式，我也不会感到惊讶。（请在评论中告诉我）结论如果你遇到了性能回退问题，红/蓝差分火焰图是找到根因的最快方式。这种方式抓取了两张普通的火焰图，然后进行对比，并对差异部分进行标色：红色表示上升，蓝色表示下降。 差分火焰图是以当前（“修改后”）的profile文件作为基准，形状和大小都保持不变。因此你通过色彩的差异就能够很直观的找到差异部分，且可以看出为什么会有这样的差异。差分火焰图可以应用到项目的每日构建中，这样性能回退的问题就可以及时地被发现和修正。via: http://www.brendangregg.com/blog/2014-11-09/differential-flame-graphs.html作者：Brendan Gregg 译者：coloka 校对：wxy本文由 LCTT 原创翻译，Linux中国 荣誉推出