IDA反汇编之栈帧例释

编程入门 行业动态 更新时间:2024-10-10 13:17:52

IDA<a href=https://www.elefans.com/category/jswz/34/1728955.html style=反汇编之栈帧例释"/>

IDA反汇编之栈帧例释

目录

1.  例释环境和预备知识

1.1 运行环境

1.2  IDA版本

1.3  预备知识

2.  函数调用约定

3.  函数局部变量布局

4.  函数栈帧示例

5.  IDA栈视图


1.  例释环境和预备知识

1.1 运行环境

本示例运行环境为Windows 10平台,所示例的程序或动态库为VS2022平台下编写并编译的X64程序。

1.2  IDA版本

本示例使用IDA Pro版,版本号7.7.220118 Win x64。

1.3  预备知识

理解本示例需要具务如下知识:

关于栈桢的约定的相关知识,详情请参见Windows平台的相关约定:

X64汇编语言寄存器结构及其与X86架构编程区别_ComputerInBook的博客-CSDN博客_x64寄存器

而关于Linux或Unix平台的栈帧的约定稍有不同,请参见:

Linux/Unix平台X64函数调用约定_ComputerInBook的博客-CSDN博客_linux x64调用约定

以上两者虽然有一些差异,但它们又有共同之处,即保有影子内存,序言和结语,以方便对于调用异常的处理。

2.  函数调用约定

与X86调用约定不同,C/C++编译器在64位平台上仅支持一种调用约定。这种调用约定利用了64位平台上可获得的新增寄存器数量:

(1)前4个整数或者指针参数依次通过rcx,rdx,r8和r9传递。

(2)前4个浮点参数通过通过前4个SSE寄存器xmm0-xmm3传递。

(3)由调用者为寄存器中的参数传递保留栈上的空间(至少在运行栈上分配32字节的阴影空间(shadow space))。被调用函数可以访问这个栈空间来,将寄存器中的内容写回栈空间。

(4) 任何多余4个参数的其它参数都使用栈来传递,并按照从左到右的次序(即,从第5个参数开始,使用栈传递参数)。

(5) 任何调用返回的整数或者指针值都放在rax寄存器中(调用完成执行返回动作时放在rax寄存器中,例如,调用ret指令时),而浮点数的返回值放在寄存器xmmO中。

(6) rax,rcx,rdx,r8-r11寄存器是易失性的(volatile)。

(7) rbx,rbp,rdi,rsi,r12-r15 寄存器是非易失性的(nonvolatile)。

这些调用约定与C++非类似:指针默认作为第一个参数传递,其它三个参数利用余下的3个寄存器,多出4个的参数则使用栈传递。

(8) call指令从rsp(堆栈指针)寄存器中减去 8,表示空出8字节的栈空间用于存放返回值,因为地址是64位长(8字节)。

(9) 当调用一个子过程(subroutine)的时候,规定指令指针(rip)必须在一个16字节的边界对齐(也就是128位,即16的倍数,这可能是在设计CPU时综合性能考量)。call指令将一个8字节的返回地址压入堆栈中,因此,调用程序必须从堆栈指针中减去8,除了32,它已经减去了阴影空间。

以下示例中,我们在单独的dll文件中定义如下形式的原型函数,因为只展示函数参数传递,我们不写函数体:

void _cdecl demo(int v1, int v2, int v3, int v4,int v5);

实现为空函数体:

void demo(int v1, int v2, int v3, int v4, int v5)

{

}

调用函数:

void Test()

{

    demo(1, 2, 3, 4,5);

}

生成可执行文件后,用IDA打开,定位到Test函数的反汇编:

(1) 切换到Imports窗口,找到调用函数demo:

(2) 切换到Imports窗口,双击显示demo函数名的这一行,进入到IDA View-A窗口:

在图中高亮行显示extrn __imp_demo:qword这一行右键,再选择交叉引用列表,出现如下交叉引用界面:

我们选中第一行,双击第一行跳转到调用函数的汇编代码(图示模式):

右键选对【文本模式】进入文本模式(或按【空格键】)在图形模式或文本模式之间切换:

(3) 我们从调用函数开始分析,即下面的反汇编代码:

在这个调用函数前后,标有---SUBROUTINE----字样,以区分调用函数体的反汇编代码起止。

下面分别分析这一段反汇编代码:

var_18= dword ptr -18h

这是IDA为方便描述指针偏移而定义的局部变量,只起辅助分析作用,不是反汇编的一部分。其命名规则为var_offset的形式,其offset为相对于基址指针的偏移量;dword ptr表示这是一个双字整数,-18表示具体的值因为后面用加的形式,因为这里用负号;h后缀表示十六进制数

sub     rsp, 38h

这一行表示在调用函数的栈空间内分配38h(56字节的栈空间),因为栈空间的分配方向是由高地址向低地,栈顶指针指向低地址,分配空间即相减。为什么全本56字节呢?从前面的预备知识我们可以了解到,四个参数的影子内存占用8x4=32个字节(注意,一个X64影子内存的大小是8字节,不管这个参数是多大),参数数量小于等于4时,默认分配28h(40字节的影子内存。参数量多于4个字节时,按每16字节递增,这里5个参数,本来再分配8字节就可以存下,由于最小分配16字节,因此分配56字节。

mov     [rsp+38h+var_18], 5

按从右向左的次序将参数入栈,因此先将5这个参数压入当前栈的偏移rsp-18h处,示意图如下:

rsp+56((=rsp+38h)

rsp+48

5

rsp+32(=rsp+38h-18h)

rsp+24

rsp+16

rsp+8

rsp

mov     r9d, 4

mov     r8d, 3

mov     edx, 2

mov     ecx, 1

按从右向左的次序将参数移入对应的寄存器参数(32位模式下是使用栈存储参数,注意与X64模式的区别),我们可以将这四个参数保存到影子内存,如果这样做了,则rsp栈指针恰好指向函数的第一个参数。

call    cs:__imp_demo_cdecl

系统开始调用子函数,注意,在call命令还做了一件事,即在进入demo_cdec函数之前,将rsp的指针减8,分配8字节的空间用于存放返回地址,调用子函数完成后,从此处取出返回地址,并跳转到进入函数之前的地址,同时rsp指针加8。(注意:X64模式会忽略前缀cs:)

add     rsp, 38h

这里与分配栈空间的地方对应,前面分配了38h字节内存,为里还原回去,从而使用得堆栈正确。

3.  函数局部变量布局

    与函数参数传递具有约定的规则不同的是,没有约定的规则指导局部变量的应该如何布局。当编译器在编译一个函数的时候,它必须面对的一个任务是计算出函数的局部变量所需要占用的空间数量。另一个任务是确定是否这些变量可以位于CPU的寄存器中,或者确定是否它们都应该位于程序栈帧中。决定将局部变量存于何处与调用函数或者任何被调函数都没有任何关系。基于检查函数源代码去确定局部变量的存储布局显然是不可能的。

4.  函数栈帧示例

定义两个函数,被调函数bar,为了演示,我们先置其实现为空;调用函数demo_stackframe,如下所示:

//callee

void bar(int j, int k)

{

}

//caller

void demo_stackframe(int a, int b, int c)

{

    int x = 0;

    char buffer[64];

    int y = 0;

    int z = 0;

    bar(z, y);

}

外层调用:

void Test()

{

    demo_stackframe(1,2,3);

}

生成可执行文件,然后执行后续操作。

(1) IDA加载可执行文件后,进步菜单【跳转】->【跳转到函数】,查找函数demo_stackframe:

(2) 反汇编代码分析

frame= byte ptr -1B0h

var_190= byte ptr -190h

var_18C= dword ptr -18Ch

k= dword ptr -10Ch

j= dword ptr -0ECh

arg_0= dword ptr  10h

arg_8= dword ptr  18h

arg_10= dword ptr  20h

下面分别说明:

frame= byte ptr -1B0h

var_190= byte ptr -190h

这两句是IDA定义的辅助变量,表示指针偏移值,byte ptr表示指针类型,后面的值表示具体偏移值。

var_18C= dword ptr -18Ch

这一句定义一个双字类型辅助变量var_18C,值为-18Ch

k= dword ptr -10Ch

j= dword ptr -0ECh

这两句定两个双字类型常量j,k。

arg_0= dword ptr  10h

arg_8= dword ptr  18h

arg_10= dword ptr  20h

这三句定义三个双字类型参数常量。

mov     [rsp-8+arg_10], r8d  ;将函数的第三个参数移入栈内存rsp-8+arg_10位置。

mov     [rsp-8+arg_8], edx ;将函数的第二个参数移入栈内存rsp-8+arg_8位置。

mov     [rsp-8+arg_0], ecx ;将函数的第一个参数移入栈内存rsp-8+arg_0位置。

push    rbp  ; rbp  指针入栈

push    rdi; rdi指针入栈

sub     rsp, 1A8h  ;分配1A8h大小的栈存储空间

lea     rbp, [rsp+20h]  ;将rsp+20h处的地址移入rbp寄存器作为栈基址

其它编译器生成的语句暂且跳过

mov     [rbp+190h+k], 0

mov     [rbp+190h+j], 0

mov     edx, [rbp+190h+k]               ; k

mov     ecx, [rbp+190h+j]               ; j

call    bar

以上语句将i,j局部变量赋值0后移入参数寄存器ecx,edx,再调用函数bar。

前面使用sub rsp, 1A8h分配了栈内存,我们看看后面释放栈内存的语句:

lea     rsp, [rbp+188h]

pop     rdi

pop     rbp

retn

本来与lea  rsp, [rbp+188h]等效的语句是add rsp, 188h,根前面的关系可以看出,

rsp = rbp-20h,因此,还原回去的需要加上1A8h ,即rsp= rbp-20h+1A8h=rbp+188h,这正是上面lea  rsp, [rbp+188h]的由来。

5.  IDA栈视图

很显然,栈帧是一个运行时概念;没有栈和没有一个正在运行的程序,栈帧是不能存在的。但是,这并不意味着你在执行诸如使用IDA等执行静态分析的时候,可以忽略栈帧的概念。为每个函数建立栈帧的所有代码都存在于二进制文件中。通过存细分析这些代码,即使程序没有运行,我们也可以获得任何函数栈帧的详细信息。事实上,IDA 的一些最复杂的分析功能是专门用于确定 IDA 反汇编的每个函数的栈帧布局。在初始分析阶段,IDA 通过记录每个push或pop操作以及任何可能改变堆栈指针的算术运算(例如添加或减去常量值),竭尽全力监控堆栈指针在函数过程中的行为。另外的目标包括确定是否一个给定函数使用了一个专用的栈帧指针(例如,通过识别一个push ebp/mov ebp,esp序列),以及识别所有引用函数栈帧局部变量的内存。

IDA栈帧视图有两种,一种是概要栈帧视图,另一种是详细栈帧视图。

查看概要栈帧视图,在引用的变量之上单击右键,比如:

mov     [rsp-8+arg_10], r8d

mov     [rsp-8+arg_8], edx

mov     [rsp-8+arg_0], ecx

在arg_10上单击右键,出现上下文菜单,有诸多操作可以在这里选择。如下图:

要查看详细栈帧视图,同样,选中对应的变量,双击,可弹出如下栈帧视图:

上图中两个特别的值需要注意:“s”和“s”(每个变量以前导空格开始),这些伪变量是 IDA 对保存的返回地址(“r”)和保存的寄存器值(“s”在本例中仅表示 EBP)的特殊表示。 为了完整性,这些值包含在栈帧视图中,因为栈帧中的每个字节都被考虑在内。

更多推荐

IDA反汇编之栈帧例释

本文发布于:2024-03-04 14:14:01,感谢您对本站的认可!
本文链接:https://www.elefans.com/category/jswz/34/1709486.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
本文标签:反汇编   IDA   栈帧例释

发布评论

评论列表 (有 0 条评论)
草根站长

>www.elefans.com

编程频道|电子爱好者 - 技术资讯及电子产品介绍!