在我的 Linux 程序中,我需要一个函数来获取地址addr
并检查callq
放置的指令是否addr
正在调用func
从共享库加载的特定函数。我的意思是,我需要检查我是否有类似callq func@PLT
at的东西addr
。
func
那么,在 Linux 上,如何从callq func@PLT
指令中获取函数的真实地址呢?
在我的 Linux 程序中,我需要一个函数来获取地址addr
并检查callq
放置的指令是否addr
正在调用func
从共享库加载的特定函数。我的意思是,我需要检查我是否有类似callq func@PLT
at的东西addr
。
func
那么,在 Linux 上,如何从callq func@PLT
指令中获取函数的真实地址呢?
只有在动态链接器解析实际加载地址之后,您才能在运行时找到相关信息。
警告:接下来是更深层次的魔法......
为了说明正在发生的事情,请使用调试器:
#include <stdio.h>
int main(int argc, char **argv) { printf("Hello, World!\n"); return 0; }
编译它(gcc -O8 ...
)。objdump -d
在二进制节目上(printf()
被替换puts()
为无法承受的普通字符串的优化......):
.init 部分的反汇编: [ ... ] .plt 部分的反汇编: 0000000000400408 < __libc_start_main@plt-0x10 >: 400408 : ff 35 a2 04 10 00 pushq 1049762(%rip) # 5008b0 <_GLOBAL_OFFSET_TABLE_+0x8> > 40040e: ff 25 a4 04 10 00 jmpq *1049764(%rip) # 5008b8 <_GLOBAL_OFFSET_TABLE_+0x10> [ ... ] 0000000000400428 <puts@plt>: 400428: ff 25 9a 04 10 00 jmpq *1049754(%rip) # 5008c8 <_GLOBAL_OFFSET_TABLE_+0x20> 40042e: 68 01 00 00 00 推 0x1 400433: e9 d0 ff ff ff jmpq 400408 <_init+0x18> [ ... ] 0000000000400500 <主>: 400500: 48 83 ec 08 sub $0x8,%rsp 400504: bf 0c 06 40 00 移动 $0x40060c,%edi 400509:e8 1a ff ff ff callq 400428 <puts@plt> 40050e: 31 c0 xor %eax,%eax 400510: 48 83 c4 08 添加 $0x8,%rsp 400514:c3 retq
现在将其加载到gdb
. 然后:
$ gdb ./tcc GNU gdb 红帽 Linux (6.3.0.0-0.30.1rh) [ ... ] (gdb) x/3i 0x400428 0x400428: jmpq *1049754(%rip) # 0x5008c8 <_GLOBAL_OFFSET_TABLE_+32> 0x40042e:pushq $0x1 0x400433:jmpq 0x400408 (gdb) x/gx 0x5008c8 0x5008c8 <_GLOBAL_OFFSET_TABLE_+32>:0x000000000040042e
请注意,此值指向直接跟在第一个 ; 后面的指令jmpq
;这意味着该puts@plt
插槽在第一次调用时将简单地“通过”到:
(gdb) x/3i 0x400408 0x400408: pushq 1049762(%rip) # 0x5008b0 <_GLOBAL_OFFSET_TABLE_+8> 0x40040e: jmpq *1049764(%rip) # 0x5008b8 <_GLOBAL_OFFSET_TABLE_+16> 0x400414:无 (gdb) x/gx 0x5008b0 0x5008b0 <_GLOBAL_OFFSET_TABLE_+8>:0x0000000000000000 (gdb) x/gx 0x5008b8 0x5008b8 <_GLOBAL_OFFSET_TABLE_+16>:0x0000000000000000
函数地址和参数尚未初始化。
这是程序加载后、执行前的状态。现在开始执行它:
(gdb) 中断主要 0x400500 处的断点 1 (gdb) 运行 启动程序:tcc (未找到调试符号) (未找到调试符号) 断点 1, 0x0000000000400500 in main() (gdb) x/i 0x400428 0x400428: jmpq *1049754(%rip) # 0x5008c8 <_GLOBAL_OFFSET_TABLE_+32> (gdb) x/gx 0x5008c8 0x5008c8 <_GLOBAL_OFFSET_TABLE_+32>:0x000000000040042e
所以这还没有改变-但目标(初始化的GOT
内容libc
)现在不同了:
(gdb) x/gx 0x5008b0 0x5008b0 <_GLOBAL_OFFSET_TABLE_+8>:0x0000002a9566b9a8 (gdb) x/gx 0x5008b8 0x5008b8 <_GLOBAL_OFFSET_TABLE_+16>:0x0000002a955609f0 (gdb)disas 0x0000002a955609f0 函数_dl_runtime_resolve的汇编代码转储: 0x0000002a955609f0 <_dl_runtime_resolve+0> : sub $0x38,%rsp [ ... ]
即在程序加载时,动态链接器将首先解析“ init
”部分。它将GOT
引用替换为重定向到动态链接代码的指针。
因此,当第一次通过引用调用外部到二进制函数时.plt
,它会再次跳转到链接器。让它这样做,然后检查程序 - 状态再次改变:
(gdb) 中断 *0x0000000000400514 0x400514 处的断点 2 (gdb) 继续 继续。 你好世界! 断点 2, 0x0000000000400514 in main() (gdb) x/i 0x400428 0x400428: jmpq *1049754(%rip) # 0x5008c8 <_GLOBAL_OFFSET_TABLE_+32> (gdb) x/gx 0x5008c8 0x5008c8:0x0000002a956c8870 (gdb)disas 0x0000002a956c8870函数puts 的汇编代码转储: 0x0000002a956c8870 <puts+0>: mov %rbx,0xffffffffffffffe0(%rsp) [ ... ]
所以现在有你的重定向libc
-最终得到解决的PLT
引用。puts()
链接器在哪里插入实际函数加载地址的指令(我们已经看到它这样做_dl_runtime_resolve
来自 ELF 二进制文件中的特殊部分:
$ readelf -a tcc [ ... ] 程序标题: 类型 偏移 VirtAddr PhysAddr FileSiz MemSiz 标志对齐 [ ... ] 中断 0x00000000000000200 0x0000000000400200 0x0000000000400200 0x000000000000001c 0x000000000000001c R 1 [请求程序解释器:/lib64/ld-linux-x86-64.so.2] [ ... ] 偏移 0x700 处的动态部分包含 21 个条目: 标签类型名称/值 0x0000000000000001(需要)共享库:[libc.so.6] [ ... ] 偏移 0x3c0 处的重定位节“.rela.plt”包含 2 个条目: 偏移信息类型 Sym。价值符号。姓名+加号 0000005008c0 000100000007 R_X86_64_JUMP_SLO 0000000000000000 __libc_start_main + 0 0000005008c8 000200000007 R_X86_64_JUMP_SLO 0000000000000000 看跌期权 + 0
ELF 不仅仅是上面的内容,但这三部分告诉内核的二进制格式处理程序“这个 ELF 二进制文件有一个解释器”(它是动态链接器)需要首先加载/初始化,它需要 libc.so.6
,并且偏移当实际执行动态链接步骤时,0x5008c0
程序0x5008c8
的可写数据部分中的 和 必须分别替换为__libc_start_main
和的加载地址。puts
从 ELF 的角度来看,这究竟是如何发生的,取决于解释器的细节(也就是动态链接器的实现)。