【发布时间】:2012-01-21 17:53:30
【问题描述】:
内存间接调用和寄存器间接调用有什么区别?
我正在尝试学习一些有关 linux rootkit 检测的知识,如何识别反汇编内存中的此类调用?编译前它们在 C 语言中的外观如何?
【问题讨论】:
内存间接调用和寄存器间接调用有什么区别?
我正在尝试学习一些有关 linux rootkit 检测的知识,如何识别反汇编内存中的此类调用?编译前它们在 C 语言中的外观如何?
【问题讨论】:
间接分支是分支到存储在寄存器或内存位置中的地址的分支。分支指令的操作数是存放分支地址的寄存器或内存位置。
查看维基百科页面了解更多信息: http://en.wikipedia.org/wiki/Indirect_branch
在 C 中,取决于实现(和 CPU),当通过函数指针调用函数时,通常会进行间接分支。由于switch 语句的一些启发式使用函数指针(通过跳转表),间接分支也可以在switch 语句中找到。
【讨论】:
内存间接调用是从内存中获取被调用者地址的调用,而寄存器间接调用则从寄存器中获取地址。
调用的实现很大程度上取决于所使用的体系结构和编译器。在 x86 上,内存和寄存器调用都是可能的,因此由编译器决定什么是最佳的。
看一个简单的测试:
#include "stdlib.h"
#include "stdio.h"
#include "time.h"
void example_fun(int param)
{
printf("%d\n", param);
}
void fn1(void)
{
printf("fn1\n");
}
void fn2(void)
{
printf("fn2\n");
}
typedef void (*fn)(void);
int main(void)
{
void (*fp) (int) = &example_fun;
int c;
fn fn_arr[] = {&fn1, &fn2};
(*fp)(2);
srand(time(NULL));
c = rand() / (RAND_MAX / 2);
switch (c)
{
case 0: (*(fn_arr[0]))(); break;
case 1: (*(fn_arr[1]))(); break;
default: (*fp)(1);
}
}
生成的程序集是(gcc 4.6.1 没有任何优化):
.file "indirect.c"
.section .rodata
.LC0:
.string "%d\n"
.text
.globl example_fun
.type example_fun, @function
example_fun:
.LFB0:
.cfi_startproc
pushl %ebp
.cfi_def_cfa_offset 8
.cfi_offset 5, -8
movl %esp, %ebp
.cfi_def_cfa_register 5
subl $24, %esp
movl $.LC0, %eax
movl 8(%ebp), %edx
movl %edx, 4(%esp)
movl %eax, (%esp)
call printf
leave
.cfi_restore 5
.cfi_def_cfa 4, 4
ret
.cfi_endproc
.LFE0:
.size example_fun, .-example_fun
.section .rodata
.LC1:
.string "fn1"
.text
.globl fn1
.type fn1, @function
fn1:
.LFB1:
.cfi_startproc
pushl %ebp
.cfi_def_cfa_offset 8
.cfi_offset 5, -8
movl %esp, %ebp
.cfi_def_cfa_register 5
subl $24, %esp
movl $.LC1, (%esp)
call puts
leave
.cfi_restore 5
.cfi_def_cfa 4, 4
ret
.cfi_endproc
.LFE1:
.size fn1, .-fn1
.section .rodata
.LC2:
.string "fn2"
.text
.globl fn2
.type fn2, @function
fn2:
.LFB2:
.cfi_startproc
pushl %ebp
.cfi_def_cfa_offset 8
.cfi_offset 5, -8
movl %esp, %ebp
.cfi_def_cfa_register 5
subl $24, %esp
movl $.LC2, (%esp)
call puts
leave
.cfi_restore 5
.cfi_def_cfa 4, 4
ret
.cfi_endproc
.LFE2:
.size fn2, .-fn2
.globl main
.type main, @function
main:
.LFB3:
.cfi_startproc
pushl %ebp
.cfi_def_cfa_offset 8
.cfi_offset 5, -8
movl %esp, %ebp
.cfi_def_cfa_register 5
andl $-16, %esp
subl $32, %esp
movl $example_fun, 24(%esp)
movl $fn1, 16(%esp)
movl $fn2, 20(%esp)
movl $2, (%esp)
movl 24(%esp), %eax
call *%eax
movl $0, (%esp)
call time
movl %eax, (%esp)
call srand
call rand
movl %eax, %ecx
movl $-2147483645, %edx
movl %ecx, %eax
imull %edx
leal (%edx,%ecx), %eax
movl %eax, %edx
sarl $29, %edx
movl %ecx, %eax
sarl $31, %eax
movl %edx, %ecx
subl %eax, %ecx
movl %ecx, %eax
movl %eax, 28(%esp)
movl 28(%esp), %eax
testl %eax, %eax
je .L6
cmpl $1, %eax
je .L7
jmp .L9
.L6:
movl 16(%esp), %eax
call *%eax
jmp .L10
.L7:
movl 20(%esp), %eax
call *%eax
jmp .L10
.L9:
movl $1, (%esp)
movl 24(%esp), %eax
call *%eax
.L10:
leave
.cfi_restore 5
.cfi_def_cfa 4, 4
ret
.cfi_endproc
.LFE3:
.size main, .-main
.ident "GCC: (Ubuntu/Linaro 4.6.1-9ubuntu3) 4.6.1"
.section .note.GNU-stack,"",@progbits
在所有情况下,我们的调用都被转换为call *%eax,这是一个内存间接调用,执行位于%eax 指向的地址的函数。添加优化会使结构混乱,但机制仍然是寄存器调用。
调用共享动态库(参见示例here)只得到call <function_name>。咨询x86 Developer Manual,call 接受寄存器、内存或指针操作数。链接器在启动时正在处理这样的调用,并且函数的名称被实际的内存地址替换。因此,调用共享库(最终)会导致内存间接调用。
除了这种容易复现的情况外,您还可以遇到直接地址的操作,例如call dword ptr ds:[00923030h](描述为here)或类似的jmp。我无法提供合理的解释它们来自哪里。
那么区分寄存器调用和内存调用就很容易了:只需查看操作数即可。
如果您对 rootkit 检测感兴趣,最好只获取一些现有的 rootkit 并并排查看其源代码和生成的程序集。
我对编译器和架构不是很有经验,所以我可能会犯一些错误。
希望对你有帮助。
【讨论】: