使用 Ftrace 破解 Linux USDT

我以前认为在 Linux 上使用用户级静态定义跟踪 (USDT) 探针是不可能的,而不添加SystemTap或LTTng。Linux 的内置跟踪器 ftrace 和 perf_events 不支持 USDT。
但我找到了一种破解它们的方法。不建议这样做。不要在家尝试做这个!

./usdt -l /opt/node/node

探测
节点:gc__start
节点:gc__done
节点:http__client__request
节点:http__client__response
[…]

./usdt 节点:gc__start

跟踪 uprobe node_gc__start (p:node_gc__start /opt/node/node:0x7f44b4)。Ctrl-C 结束。
node-23467 [001] d… 19993502.867548: node_gc__start: (0xbf44b4)
node-23484 [003] d… 19993502.983719: node_gc__start: (0xbf44b4)
node-23484 [003] d… 19993503.354810: node_gc__start: (0xbf44b4)
node-23484 [000] d… 19993503.433517: node_gc__start: (0xbf44b4)
[…]
我的 usdt 工具(在本例中,在 Node.js 上运行)是一个概念证明。重要的不是这个工具,而是在未经修改的 Linux 内核上这甚至是可能的。在这篇文章中,我将展示它是如何完成的,尽管在我们有更好的前端之前,我不鼓励任何人尝试这个。
(2017 年更新:自从这篇文章以来,我们现在有了更好的前端:perf 现在在最近的 Linux 版本中支持USDT,现在bcc/eBPF USDT 也是如此。)
用户级动态跟踪
USDT 不应与用户级动态跟踪相混淆,任何用户级代码都可以被检测。例如,通过检测服务器的dispatch_command()函数来跟踪 MySQL 服务器查询:

./uprobe ‘p:cmd /opt/bin/mysqld:_Z16dispatch_command19enum_server_commandP3THDPcj +0(%dx):string’

跟踪 uprobe cmd (p:cmd /opt/bin/mysqld:0x2dbd40 +0(%dx):string)。Ctrl-C 结束。
mysqld-2855 [001] d… 19957757.590926: cmd: (0x6dbd40) arg1=“显示表”
mysqld-2855 [001] d… 19957759.703497: cmd: (0x6dbd40) arg1=“SELECT * FROM numbers”
[…]
I’m using my uprobe tool, which in turn uses built-in Linux capabilities: ftrace (tracer) and uprobes (user-level dynamic tracing, which you’ll want a recent Linux for, eg, 4.0-ish). Other tracers, including perf_events and SystemTap, can do this as well.
Many other MySQL functions can also be traced for further insight. Listing and counting them:

./uprobe -l /opt/bin/mysqld | more

account_hash_get_key
add_collation
add_compiled_collation
add_plugin_noargs
adjust_time_range
[…]

./uprobe -l /opt/bin/mysqld | wc -l

21809
That’s 21 thousand functions. We can also trace library functions, and even individual instruction offsets.
User-level dynamic tracing is awesome, and can solve countless problems. But it can also be difficult to work with: figuring out which code to trace, processing function arguments, and dealing with code changes.
User-level Statically Defined Tracing
A USDT probe (or user-level “marker”) is where the developer has added tracing macros to their code at interesting locations, with a stable and documented API. It makes tracing easier. (If you are a developer, see Adding User Space Probing for an example of how to add these.)
With USDT, instead of tracing _Z16dispatch_command19enum_server_commandP3THDPcj, the C++ symbol for dispatch_command(), I can simply trace a probe called mysql:query__start.
I can still trace dispatch_command() if I want to, and the 21 thousand other mysqld functions, but only when needed: drilling down when the USDT probes don’t solve an issue.
USDT in Linux
Static tracepoints, in one form or another, have existed for decades. It was recently made popular by Sun’s DTrace utility, leading to them being placed in many common applications, including MySQL, PostgreSQL, Node.js, and Java. SystemTap developed a way to consume these DTrace probes.
You may be running a Linux application that already contains USDT probes, or, it may require a recompilation (usually --enable-dtrace). Check using readelf. Eg, for Node.js:

readelf -n node

[…]
Notes at offset 0x00c43058 with length 0x00000494:
Owner Data size Description
stapsdt 0x0000003c NT_STAPSDT (SystemTap probe descriptors)
Provider: node
Name: gc__start
Location: 0x0000000000bf44b4, Base: 0x0000000000f22464, Semaphore: 0x0000000001243028
Arguments: 4@%esi 4@%edx 8@%rdi
[…]
stapsdt 0x00000082 NT_STAPSDT (SystemTap probe descriptors)
Provider: node
Name: http__client__request
Location: 0x0000000000bf48ff, Base: 0x0000000000f22464, Semaphore: 0x0000000001243024
Arguments: 8@%rax 8@%rdx 8@-136(%rbp) -4@-140(%rbp) 8@-72(%rbp) 8@-80(%rbp) -4@-144(%rbp)
[…]
This is node recompiled with --enable-dtrace, and with the systemtap-sdt-dev package, which provided a “dtrace” utility to build USDT support. Two probes are shown here: node:gc__start (garbage collection start), and node:http__client__request.
At this point you can use SystemTap or LTTng to trace these. The built-in Linux tracers, ftrace and perf_events, cannot (although support for perf_events is in development). However…
Hacking with Ftrace: Tracing Addresses
From the readelf output above, node:gc__start is at 0xbf44b4. What is this address?
$ gdb node
(gdb) print/a 0xbf44b4
$1 = 0xbf44b4 <_ZN4node15dtrace_gc_startEPN2v87IsolateENS0_6GCTypeENS0_15GCCallbackFlagsE+4>
(gdb) disas _ZN4node15dtrace_gc_startEPN2v87IsolateENS0_6GCTypeENS0_15GCCallbackFlagsE
Dump of assembler code for function _ZN4node15dtrace_gc_startEPN2v87IsolateENS0_6GCTypeENS0_15GCCallbackFlagsE:
0x0000000000bf44b0 <+0>: push %rbp
0x0000000000bf44b1 <+1>: mov %rsp,%rbp
0x0000000000bf44b4 <+4>: nop
0x0000000000bf44b5 <+5>: pop %rbp
0x0000000000bf44b6 <+6>: retq
End of assembler dump.
It’s a nop (no-operation). Ftrace and uprobes can instrument instructions, so we can instrument this address.
Since this is not a shared library, we need to know and subtract the base load address:

objdump -x /opt/node/node | more

/opt/node/node: file format elf64-x86-64
/opt/node/node
architecture: i386:x86-64, flags 0x00000112:
EXEC_P, HAS_SYMS, D_PAGED
start address 0x0000000000617770

Program Header:
PHDR off 0x0000000000000040 vaddr 0x0000000000400040 paddr 0x0000000000400040 align 23
filesz 0x00000000000001f8 memsz 0x00000000000001f8 flags r-x
INTERP off 0x0000000000000238 vaddr 0x0000000000400238 paddr 0x0000000000400238 align 2
0
filesz 0x000000000000001c memsz 0x000000000000001c flags r–
LOAD off 0x0000000000000000 vaddr 0x0000000000400000 paddr 0x0000000000400000 align 221
filesz 0x0000000000c2ada4 memsz 0x0000000000c2ada4 flags r-x
LOAD off 0x0000000000c2bca8 vaddr 0x000000000122bca8 paddr 0x000000000122bca8 align 2
21
filesz 0x0000000000017384 memsz 0x0000000000020cb0 标志 rw-
[…]
它是 0x400000(x86_64 的典型值)。uprobes 文档uprobetracer.txt是从 /proc/PID/maps 获取的,但是,该技术需要一个正在运行的进程。
从 0xbf44b4 = 0x7f44b4 中减去 0x400000,这是与 uprobes 一起使用的地址(因为它不是共享库)。使用我的 uprobe 工具(前面提到过)跟踪它:
警告:地址必须正确且指令对齐

./uprobe ‘p:gc_start node:0x7f44b4’

跟踪 uprobe gc_start (p:gc_start /opt/node/node:0x7f44b4)。Ctrl-C 结束。
节点 23484 [002] d… 19993896.988891: gc_start: (0xbf44b4)
节点 23467 [003] d… 19993897.079658: gc_start: (0xbf44b4)
节点 23484 [003] d… 19993897.326284: gc_start: (0xbf44b4)
节点 23484 [003] d… 19993897.402107: gc_start: (0xbf44b4)
节点 23467 [001] d… 19993897.605167: gc_start: (0xbf44b4)
[…]
整洁的。我给它起了别名“gc_start”。我们可以从两个节点进程中看到 GC。
这是我们更喜欢一个更好的前端,一个有更多安全检查的东西(例如,perf_events)。Ftrace 假设您知道自己在做什么。
perf_events>看
你看到了一杯毒药。

perf_events>饮料杯
我害怕我不会那样做。因为它是毒药。

ftrace>饮料杯
咕噜咕噜咕噜!
如果你弄错了地址,而且你很幸运,目标进程将立即崩溃(“非法指令”)。如果运气不好,它将处于未知的损坏状态。
使用 Ftrace 进行黑客攻击:已启用
让我们将前面的技术与我突出显示的第二个探针一起使用:node:http__client__request:

./uprobe p:node:0x7f48ff

跟踪 uprobe node_0x7f48ff (p:node_0x7f48ff /opt/node/node:0x7f48ff)。Ctrl-C 结束。
^C
结束追踪…
没有什么。这个探测器应该被触发(有负载),但不是。原因在readelf输出中:
位置:0x0000000000bf48ff,基础:0x0000000000f22464,信号量:0x0000000001243024
此探测器使用信号量来实现 is-enabled:DTrace 的一项功能,其中跟踪器可以通知目标进程正在跟踪特定事件。然后目标进程可以选择执行一些更昂贵的处理,通常为 USDT 探测获取和格式化参数。
我们需要设置信号量。当前值应为零,并且是:

dd if=/proc/12647/mem bs=1 count=1 skip=$(( 0x1243026 )) 2>/dev/null | xxd

0000000: 00 .
让我们将其设置为 1(假设它从零开始):
警告:这是一个概念证明,不适合实际使用

printf “\x1” | dd of=/proc/12647/mem bs=1 count=1 seek=$(( 0x1243024 ))

1+0 条记录在
1+0 条记录
1 个字节 (1 B) 已复制,3.5286e-05 s,28.3 kB/s

dd if=/proc/12647/mem bs=1 count=1 skip=$(( 0x1243024 )) 2>/dev/null | xxd

0000000: 01 .
是的,我将 bash 的输出直接传递到目标内存上。您可能永远不应该这样做!
它确实有效,我现在可以跟踪探针:

./uprobe ‘p:client_request /opt/node/node:0x7f48ff’ | 头-5

跟踪 uprobe client_request (p:client_request /opt/node/node:0x7f48ff)。Ctrl-C 结束。
节点 12647 [001] d… 20025472.106062: client_request: (0xbf48ff)
node-12647 [001] d … 20025472.106601:client_request:(0xbf48ff)
节点 12647 [001] d… 20025472.107240: client_request: (0xbf48ff)
节点 12647 [001] d… 20025472.107813: client_request: (0xbf48ff)
好的。完成后我应该减少信号量。
这需要一个更好的前端来正确执行信号量。我的 bash one-liner 没有错误检查。
bash|dd|/dev/mem>看
你站在森林里。您会看到向北的目标流程。

bash|dd|/dev/mem>北
我不明白“北方”。你完蛋了。
如果你弄错了地址,你会破坏内存。
使用 Ftrace 进行黑客攻击:参数
既然我们可以追踪 USDT 探测,那么他们的论点呢?它们也显示在readelf中,例如:
名称:gc__start
参数:4@%esi 4@%edx 8@%rdi

名称:http__client__request
参数:8@%rax 8@%rdx 8@-136(%rbp) -4@-140(%rbp) 8@-72(%rbp) 8@-80(%rbp) -4@- 144(%rbp)

这显示了它们的寄存器和堆栈位置。
gc__start的参数可以从构建 USDT 探针的节点源中确定。例如:
src/node_provider.d:
54 提供者节点 {
[…]
78 探针 gc__start(int t, int f, void *isolate);

src/node_dtrace.cc:
293 void dtrace_gc_start(隔离*隔离,GCType类型,GCCallbackFlags标志){
294 // 此探测点的先前版本仅记录类型和标志。
295 // 这就是为什么出于向后兼容性的原因,隔离在最后。
296 NODE_GC_START(类型,标志,隔离);
297 }
所以第一个参数是“type”,存储在4@%esi : size 4,寄存器 %esi。(这是基于汇编的语法参考。)跟踪它,并将其命名为“gctype”:

./uprobe ‘p:client_request /opt/node/node:0x7f44b4 gctype=%si:u32’

跟踪 uprobe client_request (p:client_request /opt/node/node:0x7f44b4 gctype=%si:u32)。Ctrl-C 结束。
node-12617 [000] d … 20031814.978011:client_request:(0xbf44b4)gctype = 0x1
节点 12647 [002] d… 20031814.985857: client_request: (0xbf44b4) gctype=0x1
节点 12647 [001] d… 20031815.323407: client_request: (0xbf44b4) gctype=0x1
node-12647 [001] d … 20031815.412093:client_request:(0xbf44b4)gctype = 0x2
node-12617 [000] d … 20031815.535844:client_request:(0xbf44b4)gctype = 0x1
[…]
伟大的!来自 deps/v8/include/v8.h:类型 1 是 scavange,类型 2 是 mark-sweep-compact。
其他论点可能会变得很棘手,但对于真正确定的人来说仍然是可能的。例如,获取node:http__client__request的 url 和方法:

./uprobe ‘p:client_request /opt/node/node:0x7f48ff +0(+0(%ax)):string’ | 头-5

跟踪 uprobe client_request (p:client_request /opt/node/node:0x7f48ff +0(+0(%ax)):string)。Ctrl-C 结束。
节点 12647 [003] d… 20031007.438200: client_request: (0xbf48ff) arg1=“/”
节点 12647 [003] d… 20031007.438776: client_request: (0xbf48ff) arg1=“/”
节点 12647 [003] d… 20031007.439322: client_request: (0xbf48ff) arg1=“/”
节点 12647 [003] d… 20031007.439921: client_request: (0xbf48ff) arg1=“/”

./uprobe ‘p:client_request /opt/node/node:0x7f48ff +0(+8(%ax)):string’ | 头-5

跟踪 uprobe client_request (p:client_request /opt/node/node:0x7f48ff +0(+8(%ax)):string)。Ctrl-C 结束。
节点 12647 [000] d… 20032205.934697: client_request: (0xbf48ff) arg1=“GET”
节点 12647 [000] d… 20032205.935255: client_request: (0xbf48ff) arg1=“GET”
node-12647 [000] d … 20032205.935832:client_request:(0xbf48ff)arg1 =“GET”
节点 12647 [000] d… 20032205.936367:client_request: (0xbf48ff) arg1=“GET”
你的参数+0(+8(%ax)):string以寄存器 (%ax) 开头,然后取消引用偏移量 (+8(),然后 +0),然后强制转换它 (:string)。这种语法类似于早期的参数符号,但基本上仍然是brainf*ck。它记录在uprobetracer.txt和kprobetrace.txt 中(参见 FETCHARGS),但我不知道它是否有真名。
但与其他黑客行为一样,真正的重点是我无需修改内核就可以让它工作。我们想要的是更好的前端。
更安全的黑客攻击
现在我已经证明了这是可能的,我将总结一些不同的方法来使其更安全。
用 C 重写上面的内容。包括错误和安全检查。
父函数的跟踪。较早的node::gc__start由dtrace_gc_start()直接调用,我们可以使用用户级动态跟踪进行跟踪,如 ftrace、perf_events 和其他跟踪器支持。检查代码:这在探测宏和函数入口之间没有分支时有效,因此跟踪它们本质上是相同的。
跟踪另一个金丝雀函数。如果父函数不合适,则代码中可能有一个先前唯一的函数调用,并且可以使用用户级动态跟踪进行跟踪。
通过函数 offset 进行跟踪。可以使用父函数的偏移量来跟踪 USDT 探测位置。例如,对于 perf_events,使用perf probe -x binary ‘func+offset’。这可能更安全,因为它可能涉及更多的错误检查(取决于跟踪器),包括确保偏移量在函数的范围内,并且它是指令对齐的。
使用另一个跟踪器跟踪 USDT。SystemTap或LTTng支持 USDT。
等待 perf_events 或 ftrace 支持。到目前为止,至少有两个人致力于适当的 perf_events USDT 支持。等不及了!
至于我的原型usdt工具:

./usdt -ip 12647 ‘node:http__client__request +0(+0(%ax)):string’

跟踪 uprobe node_http__client__request (p:node_http__client__request /opt/node/node:0x7f48ff +0(+0(%ax)):string)。Ctrl-C 结束。
node-12647 [001] d… 20047728.328532: node_http__client__request: (0xbf48ff) arg1=“/”
node-12647 [001] d… 20047728.329050: node_http__client__request: (0xbf48ff) arg1=“/”
node-12647 [001] d… 20047728.329577: node_http__client__request: (0xbf48ff) arg1=“/”
node-12647 [001] d… 20047728.330153: node_http__client__request: (0xbf48ff) arg1=“/”
node-12647 [001] d… 20047728.330689: node_http__client__request: (0xbf48ff) arg1=“/”
[…]

./usdt -h

用法:usdt [-FhHisv] [-d secs] [-p PID] {[-lL] 目标 |
usdt_probe [过滤器]}
-F#力。尽管有警告,仍可追踪。
-d seconds # 跟踪持续时间,并使用缓冲区
-i # enable isenabled 探测。需要-p。
# 警告:写入目标内存。
-l target # 列出这个可执行文件中的 usdt 探针
-L target # 列出 usdt 探针和参数
-p PID #要匹配的PID
-v # 查看格式文件(不跟踪)
-H # 包含列标题
-s # 显示用户堆栈跟踪
-h # 这个用法信息
[…]
对于前面描述的警告,我认为以目前的形式分享它是鲁莽的。一旦我的公司使用更新的内核,我将考虑提高其安全性,并且我们更有可能使用它(它使用 uprobes,这在我们正在运行的内核上不稳定:很多 3.13)。
perf_events USDT 支持可能会在今年晚些时候到来,我可以重写 uprobe 来使用它。这会很好,但同时还有其他选项,包括用于 USDT 的 SystemTap,以及用于用户级动态跟踪的 ftrace 和 perf_events。这不像我们错过了。
结论
有一种使用内置 Linux 内核功能访问 USDT 探测的方法:ftrace 和 uprobes。现在这是一个概念证明,我们缺乏一个像样的前端工具,可以进行错误和安全检查。(但您可能有迫切的需求,无论如何了解这一点很有用;尽管如此,请检查 SystemTap 和 LTTng。)将来,内置的 USDT 支持只能从这里变得更好!

转载翻译自: https://www.brendangregg.com