首页 > 编程语言 >【逆向】x64程序逆向基础——调用约定和栈使用

【逆向】x64程序逆向基础——调用约定和栈使用

时间:2023-08-02 18:38:40浏览次数:28  
标签:逆向 调用 函数 x64 mov eax 参数 寄存器 rsp

【逆向】x64程序逆向基础

 

主要区别

1. 所有地址指针都是64位。
2. 增加和扩展新的寄存器,并兼容原32位版本的通用寄存器。
3. 原指令指针寄存器EIP扩展为RIP。

寄存器

1. 64位寄存器兼容原32位寄存器。
2. 新增加8个XMM寄存器(XMM8-XMM15)。
3. 扩展原32位寄存器的64位版本,并增加8个新的64位寄存器(R8-R15)。
// 通用寄存器:RAX(64位),EAX(32位),AX(16位),AL(0-7位),AH(8-15位)
// 新增寄存器:R8(64位),R8D(32位),R8W(16位),R8B(8位)

调用约定

1. x86使用stdcall、cdecl、Fastcall等。
2. x64使用类似“Fastcall”的调用约定。
使用RCX、RDX、R8、R9寄存器传递前4个参数,其余参数从右往左依次保存在栈上。
3. 浮点参数使用XMM寄存器传递(XMM0-XMM3)。
4. 任何在函数开头的mov指令都是在保存被传递到这个函数的参数,编译器不会再其中插入做其它事情的mov指令。
1 mov dword ptr [rsp+28h] ,6     //参数6 保存在栈中
2 mov dword ptr [rsp+20h] ,5     //参数5 保存在栈中
3 mov r9d ,4                     //参数4 保存在寄存器中
4 mov r8d ,3                     //参数3 保存在寄存器中
5 mov edx ,2                     //参数2 保存在寄存器中
6 mov ecx ,1                     //参数1 保存在寄存器中
7 call Fun                       //调用函数

栈使用

1. 32位代码在函数中使用push和pop等指令改变栈的大小。
2. 64位代码在函数中从不改变栈的大小,栈在函数的开始增长,期间一直保持不变,直到函数末尾。
3. 当一个函数调用另一个函数时,调用函数会多申请32字节(0x20)的预留栈空间,当被调用函数寄存器不够用时,可以将4个参数寄存器(RCX、RDX、R8、R9)中的值保存在申请的预留栈空间中。
预留栈空间由函数调用者提前申请,也由函数调用者负责平衡回收。
注意:如果一个函数有其他参数(>4个)或局部栈变量,函数会在0x20的基础上增加预留栈空间的大小,有时增加大小后的值需要与16进行对齐。

示例代码

1 #include "stdafx.h"
 2 
 3 // Add
 4 int Add(int nl, int n2, int n3, int n4, int n5, int n6)
 5 {
 6     return nl+n2+n3+n4+n5+n6;
 7 }
 8 
 9 // Main
10 int tmain(int argc, TCHAR* argv[])
11 {
12     printf("%d\r\n", Add(1,2,3,4,5,6));
13     return 0;
14 }

 Main函数反汇编

1 // 保存Main函数参数到预留栈空间,此预留栈空间为其它函数调用Main函数时申请
 2 mov[rsp+10h], rdx            // 将参数2保存到预留栈空间中
 3 mov[rsp+8h], ecx             // 将参数1保存到预留栈空间中
 4 
 5 // Main函数作为调用者申请预留栈空间,用于保存Add函数的参数
 6 push rdi                     // 保存环境
 7 sub rsp, 30h                 // 申请预留栈空间(Add函数6个参数)(6*8=48 0x30)
 8 mov rdi, rsp                 // 将栈空间初始化为0xcC
 9 mov ecx, 0Ch
10 mov eax, 0CCCCCCCCh
11 rep stosd
12 
13 // 调用Add函数,前4个参数使用寄存器,其余参数入栈
14 mov ecx, [rsp+40h]
15 mov dword ptr [rsp+28h], 6   // 参数6入栈
16 mov dword ptr [rsp+20h], 5   // 参数5入栈
17 mov r9d, 4                   // 参数4
18 mov r8d, 3                   // 参数3
19 mov edx, 2                   // 参数2
20 mov ecx, 1                   // 参数1
21 cal1 Add                     // 调用Add函数
22 
23 // 调用pirntf函数
24 mov edx, eax                 // 将返回值保存到edx中
25 lea rcx, Format              // "%d\r\n"
26 cal1 printf                  // 调用pirntf函数
27 xor eax, eax                 // 设置返回值
28 
29 // Main函数作为调用者释放预留栈空间
30 add rsp, 30h                 // 释放预留栈空间+2个参数的栈空间(Add参数5,6)
31 pop rdi                      // 恢复环境
32 retn                         // 函数返回

 Add函数反汇编

1 // 保存Add函数前4个参数到预留栈空间,预留栈空间由Mian函数申请和释放
 2 mov[rsp + 20h], r9d     // 参数4
 3 mov[rsp + 18h], r8d     // 参数3
 4 mov[rsp + 10h], edx     // 参数2
 5 mov[rsp + 08h], ecx     // 参数1
 6 
 7 // Add函数中没有调用其它函数和局部变量,所以没有申请预留栈空间
 8 push rdi               // 保存环境
 9 mov eax, [rsp + 18h]    // eax = 参数2
10 mov ecx, [rsp + 10h]    // ecx = 参数1
11 add ecx, eax           // ecx = 参数1+参数2
12 mov eax, ecx           // eax = ecx
13 
14 // 使用预留栈空间来获取Add函数参数
15 add eax, [rsp + 20h]    // eax+参数3
16 add eax, [rsp + 28h]    // eax+参数4
17 add eax, [rsp + 30h]    // eax+参数5
18 add eax, [rsp + 38h]    // eax+参数6
19 
20 // 再次印证预留栈空间由调用函数(Main函数)释放
21 pop rdi                // 恢复环境
22 retn                   // 函数返回

 

注意里面有一个细节:main里调用的时候,

16 mov dword ptr [rsp+20h], 5 // 参数5入栈

而add里取出来的时候,是:

17 add eax, [rsp + 30h]    // eax+参数5

刚好相差了16,理由是call的时候push了RIP,同时在add里有push rdi


三、x64函数约定

在默认情况下,x64程序使用fastcall函数约定,与大部分32位程序最大的区别就是他的参数并不是通过push来进行传递的,而是默认将前4个参数存放在rcx,rdx,r8,r9中。

【逆向】x64程序逆向基础——调用约定和栈使用_逆向分析

图中我们传递是4个int型数值,所以传递的寄存器为低32位寄存器,不难看出,这个x64程序是符合我们上面所说的函数约定的。

但是并不是所有的函数都只会用到不超过4个参数,所以,我们还要考虑有更多参数的情况。

当参数多于4个时,x64程序会将多出来的参数传递给[rsp+0x20],[rsp+0x28]......[rsp+(n+1)*8]

【逆向】x64程序逆向基础——调用约定和栈使用_调用函数_02

图中的第5个参数传递给了[rsp+0x20]。

有人可能在翻阅资料的时候会看到,有的人说第5个参数是[rsp+28],这种说法也没错,因为这两种情况观察参数的位置是不同的

如果我们在函数调用处观察参数,第5个参数是[rsp+0x20],如果我们在函数内部去观察,由于步进call的过程中会push RIP,所以此时的rsp是要-8的,所以第五个参数自然就变成了[rsp+0x28]。

虽然在x64程序中,我们默认的前4个参数是rcx-r9,但是也有例外,如果我们传递的参数是浮点型的话,那么传入浮点数的参数则会使用xmm0-xmm3来代替。

【逆向】x64程序逆向基础——调用约定和栈使用_逆向分析_03

图中的第3个参数,也就是原本的r8的位置,此时已经变成了xmm2,而r8并没有做为参数传递到函数里。也就是说,如果在前4个参数中存在浮点数的话,那么,它所对应的4个通用寄存器则会被xmm寄存器所替代。

现在参数我们已经有个大概的了解了,接下来则是函数的返回值。

之前我们说过,函数的返回值是存放在rax寄存器中的,这一点和32位程序是一样的。

【逆向】x64程序逆向基础——调用约定和栈使用_函数调用_04

图中的返回值是传递给rax,如果我们返回的是个int型数值的话,那么返回值还是会像32位程序一样传给eax的。

但是这个返回值也会有例外,比如我们要返回一个浮点型的话,那么此时的返回值就会传递给xmm0,在函数执行之后rax就没有用武之地了。

【逆向】x64程序逆向基础——调用约定和栈使用_寄存器_05

在返回 __m128、 __m128i、 __m128d、float、double时,返回值会传递给xmm0,其他情况则会传递给rax。

 

 

四、x64的堆栈

32位的堆栈可以说是初学者的噩梦,很多人在学习堆栈时耗费了大量的时间,这不仅仅是因为堆栈先进后出的抽象概念,同样也是因为32位的函数运行中常常使用push,pop来传递参数和维持堆栈平衡。

x64程序的函数约定可以说是初学者的福音,在汇编代码中没有了满窗口的push、pop、add esp,xxx、sub esp,xxx,这使得堆栈的运算变得格外的简单。

【逆向】x64程序逆向基础——调用约定和栈使用_函数调用_06

比如图中我们想要分析[rsp+F8]的来源,如果他是作为参数来源于上面的某个CALL中的话,那么我们只需要高亮F8,就可以轻松的找到他所在的CALL,并不需要担心rsp在这个过程中的变化,因为rsp基本上不会改变。很明显,他的来源就在上面的一个call里

【逆向】x64程序逆向基础——调用约定和栈使用_调用函数_07

这里他作为第三个参数传递到了call里,那么如果他不是来源于某个call里,而是来源于外层的呢?那也很简单,我们直接来到函数头部,计算一下他在函数头部是rsp+?,然后在返回接着去分析就可以了。

【逆向】x64程序逆向基础——调用约定和栈使用_调用函数_08

比如这里有一个[rsp+90],他并没有来源于本层调用的某个函数,那么我们只需要在头部进行计算,减去push rdi和sub rsp,60改变的偏移,变成[rsp+0x28],再减去push RIP的8个字节,就可以得出他来源于外层的[rsp+20],很明显这是第五个参数

【逆向】x64程序逆向基础——调用约定和栈使用_调用函数_09

执行到返回后我们会发现这个函数的确有第五个参数,他的来源是r12。

在一些程序中,我们还会发现如下代码

【逆向】x64程序逆向基础——调用约定和栈使用_逆向分析_10

此时的寻址方式并不是以rsp为基地址来传递局部变量和参数,那么如果我们想知道一个rbp+xxxx是局部变量还是参数,就需要到头部去进行一个相对复杂的运算,比如图中的rcx来源rbp-59,虽然我们明知他是一个局部变量(因为前面是lea),但是我们也要到头部去算一算,-59-5F= -B8,在头部的地址是rsp-B8,也就是说他是一个局部变量。

这种方式看似复杂,其实很简单,因为他不需要去计算头部的push和sub rsp,事实上这些偏移都是相对于头部的RIP的。

类似的代码还有很多种,不过我们如果单纯的去逆向数据的话,不必考虑这些寻址方式,只要能正确的去进行计算就可以了。

 

主要区别

1. 所有地址指针都是64位。
2. 增加和扩展新的寄存器,并兼容原32位版本的通用寄存器。
3. 原指令指针寄存器EIP扩展为RIP。

寄存器

1. 64位寄存器兼容原32位寄存器。
2. 新增加8个XMM寄存器(XMM8-XMM15)。
3. 扩展原32位寄存器的64位版本,并增加8个新的64位寄存器(R8-R15)。
// 通用寄存器:RAX(64位),EAX(32位),AX(16位),AL(0-7位),AH(8-15位)
// 新增寄存器:R8(64位),R8D(32位),R8W(16位),R8B(8位)

调用约定

1. x86使用stdcall、cdecl、Fastcall等。
2. x64使用类似“Fastcall”的调用约定。
使用RCX、RDX、R8、R9寄存器传递前4个参数,其余参数从右往左依次保存在栈上。
3. 浮点参数使用XMM寄存器传递(XMM0-XMM3)。
4. 任何在函数开头的mov指令都是在保存被传递到这个函数的参数,编译器不会再其中插入做其它事情的mov指令。
1 mov dword ptr [rsp+28h] ,6     //参数6 保存在栈中
2 mov dword ptr [rsp+20h] ,5     //参数5 保存在栈中
3 mov r9d ,4                     //参数4 保存在寄存器中
4 mov r8d ,3                     //参数3 保存在寄存器中
5 mov edx ,2                     //参数2 保存在寄存器中
6 mov ecx ,1                     //参数1 保存在寄存器中
7 call Fun                       //调用函数

栈使用

1. 32位代码在函数中使用push和pop等指令改变栈的大小。
2. 64位代码在函数中从不改变栈的大小,栈在函数的开始增长,期间一直保持不变,直到函数末尾。
3. 当一个函数调用另一个函数时,调用函数会多申请32字节(0x20)的预留栈空间,当被调用函数寄存器不够用时,可以将4个参数寄存器(RCX、RDX、R8、R9)中的值保存在申请的预留栈空间中。
预留栈空间由函数调用者提前申请,也由函数调用者负责平衡回收。
注意:如果一个函数有其他参数(>4个)或局部栈变量,函数会在0x20的基础上增加预留栈空间的大小,有时增加大小后的值需要与16进行对齐。

示例代码

1 #include "stdafx.h"
 2 
 3 // Add
 4 int Add(int nl, int n2, int n3, int n4, int n5, int n6)
 5 {
 6     return nl+n2+n3+n4+n5+n6;
 7 }
 8 
 9 // Main
10 int tmain(int argc, TCHAR* argv[])
11 {
12     printf("%d\r\n", Add(1,2,3,4,5,6));
13     return 0;
14 }

 Main函数反汇编

1 // 保存Main函数参数到预留栈空间,此预留栈空间为其它函数调用Main函数时申请
 2 mov[rsp+10h], rdx            // 将参数2保存到预留栈空间中
 3 mov[rsp+8h], ecx             // 将参数1保存到预留栈空间中
 4 
 5 // Main函数作为调用者申请预留栈空间,用于保存Add函数的参数
 6 push rdi                     // 保存环境
 7 sub rsp, 30h                 // 申请预留栈空间(Add函数6个参数)(6*8=48 0x30)
 8 mov rdi, rsp                 // 将栈空间初始化为0xcC
 9 mov ecx, 0Ch
10 mov eax, 0CCCCCCCCh
11 rep stosd
12 
13 // 调用Add函数,前4个参数使用寄存器,其余参数入栈
14 mov ecx, [rsp+40h]
15 mov dword ptr [rsp+28h], 6   // 参数6入栈
16 mov dword ptr [rsp+20h], 5   // 参数5入栈
17 mov r9d, 4                   // 参数4
18 mov r8d, 3                   // 参数3
19 mov edx, 2                   // 参数2
20 mov ecx, 1                   // 参数1
21 cal1 Add                     // 调用Add函数
22 
23 // 调用pirntf函数
24 mov edx, eax                 // 将返回值保存到edx中
25 lea rcx, Format              // "%d\r\n"
26 cal1 printf                  // 调用pirntf函数
27 xor eax, eax                 // 设置返回值
28 
29 // Main函数作为调用者释放预留栈空间
30 add rsp, 30h                 // 释放预留栈空间+2个参数的栈空间(Add参数5,6)
31 pop rdi                      // 恢复环境
32 retn                         // 函数返回

 Add函数反汇编

1 // 保存Add函数前4个参数到预留栈空间,预留栈空间由Mian函数申请和释放
 2 mov[rsp + 20h], r9d     // 参数4
 3 mov[rsp + 18h], r8d     // 参数3
 4 mov[rsp + 10h], edx     // 参数2
 5 mov[rsp + 08h], ecx     // 参数1
 6 
 7 // Add函数中没有调用其它函数和局部变量,所以没有申请预留栈空间
 8 push rdi               // 保存环境
 9 mov eax, [rsp + 18h]    // eax = 参数2
10 mov ecx, [rsp + 10h]    // ecx = 参数1
11 add ecx, eax           // ecx = 参数1+参数2
12 mov eax, ecx           // eax = ecx
13 
14 // 使用预留栈空间来获取Add函数参数
15 add eax, [rsp + 20h]    // eax+参数3
16 add eax, [rsp + 28h]    // eax+参数4
17 add eax, [rsp + 30h]    // eax+参数5
18 add eax, [rsp + 38h]    // eax+参数6
19 
20 // 再次印证预留栈空间由调用函数(Main函数)释放
21 pop rdi                // 恢复环境
22 retn                   // 函数返回

标签:逆向,调用,函数,x64,mov,eax,参数,寄存器,rsp
From: https://blog.51cto.com/u_11908275/6941481

相关文章

  • C++逆向分析——友元、内部类、命名空间和static
    友元友元可以理解为:朋友、元素;老师认为这个友元是C++中的一个垃圾,因为友元的存在破坏了面向对象的封装性,不推荐使用,之所以有这个章节是因为有人不了解这个概念。注意:在一些新版本的C++编译器里面已经不再提供类似于友元这样的特性了。大家都知道在C++中对象的私有成员,外部是无法访......
  • C++逆向分析——对象拷贝
    对象拷贝我们通常存储对象,都用数组、列表之类的来存储,那如下所示我们使用数组来存储对象,但是在工作中发现这个数组不够用了,就需要一个更大的数据,但我们重新创建一个数组还需要把原来的数据复制过来;在C语言中可以使用函数来进行拷贝,直接拷贝内存,在C++中实际上跟C语言要做的事情是一......
  • C++逆向分析——模版
    模版假设有一个冒泡排序的函数:voidSort(int*arr,intnLength){inti,k;for(i=0;i<nLength;i++){for(k=0;k<nLength-1-i;k++){if(arr[k]>arr[k+1]){inttemp=arr[k];arr[k]=arr[k+1];arr[k+1]=temp;}}}}但是这个冒......
  • C++逆向分析——运算符重载
    运算符重载现在有一个类,其中有一个函数用于比较2个类的成员大小:#include<stdio.h>classNumber{private:intx;inty;public:Number(intx,inty){this->x=x;this->y=y;}intMax(Number&n){returnthis->x>n.x&&this->y......
  • C++逆向分析——多态和虚表
    虚表上一章了解了多态,那么我们来了解一下多态在C++中是如何实现的。了解本质,那就通过反汇编代码去看就行了,首先我们看下非多态的情况下的反汇编代码:然后再来看下多态情况下的反汇编代码:很明显这里多态的情况下会根据edx间接调用,而非多态则会直接调用。那么我们来看下间接调用的流程......
  • C++逆向分析——继承与封装
    面向对象程序设计之继承与封装之前已经学习过继承和封装了,但是要在实际开发中使用,光学语法和原理是不够的,在设计层面我们需要做一些优化。如下代码是继承的例子:#include<stdio.h>classPerson{public:intAge;intSex;voidWork(){printf("Person:Work()"......
  • C++逆向分析——引用
    voidmain(){intx=1;int&ref=x;ref=2;printf("%d\n",ref);return;}反汇编代码:intx=1;00724A5FC745F401000000movdwordptr[x],1int&ref=x;00724A668D45F4lea......
  • C++逆向分析——类成员的访问控制
    类成员的访问控制课外→好的编程习惯:定义与实现分开写,提升代码可读性。如下代码,Student这个类的所有成员我们都可以调用,但是我们不想让被人调用Print1这个方法该怎么?structStudent{intage;intsex;voidPrint1(){printf("FuncPrint1");}voidPrint(){......
  • C++逆向分析——new和delete new[]和delete[]
    在堆中创建对象我们可以在什么地方创建对象?全局变量区,在函数外面在栈中创建对象,也就是函数内在堆中创建对象注意:之前一直提到的堆栈实际上是两个概念->堆、栈,我们之前所讲的就是栈,从本章开始要严格区分。在C语言中,我们可以通过一个函数去申请一块内存,就是malloc(N);申请的这一块内存......
  • C++逆向分析——继承
    继承structPerson{intage;intsex;};structTeacher{intage;intsex;intlevel;intclassId;};如上代码中可以看见,Teacher类与Person类都存在着相同的2个成员age和sex,那么这就相当于重复编写了,我们可以通过继承的方式避免这样重复的编写(当前类名称:要......