X64逆向
64位相比于32位的优势:
更大的地址空间:64位系统可以访问的内存地址空间比32位系统大得多。具体来说,32位系统最多可以使用4GB的内存(2^32字节),而64位系统则可以使用高达16EB(2^64字节)的内存。这对于需要大量内存的应用程序,如大型数据库、视频编辑软件和虚拟化应用程序等,尤为重要。
更高的性能:在某些情况下,64位程序可以利用更多的寄存器和更宽的数据路径,从而提高性能。例如,64位处理器可以一次性处理更大的整数和浮点数,这对于科学计算、加密和多媒体处理等任务有显著的性能提升。
增强的安全性:64位系统通常包含更多的安全功能,例如硬件层面的地址空间布局随机化(ASLR)和数据执行保护(DEP)。这些功能可以帮助防范某些类型的攻击,如缓冲区溢出攻击。
兼容性和未来性:随着技术的发展,越来越多的软件和操作系统开始只支持64位版本。使用64位程序可以确保更好的兼容性和未来的支持,因为软件和硬件供应商将逐渐减少对32位系统的支持。
更好的多任务处理能力:64位处理器在处理多任务和多线程应用程序时,表现通常优于32位处理器。这对于需要同时运行多个应用程序或处理大量并发任务的环境非常有利。
x64位CPU的三种运行模式:
- 实模式:在此模式下,可以跑16位程序。微软的操作系统禁用此模式
- 64位模式:只能运行x64位的程序。
- 兼容模式:能运行x32位和x64位程序。模拟执行32位程序。
有个坑点
mov eax,0 和 mov rax,0效果是一致的,虽然只操作了eax,但是顺便也会把高位置为0
所以在64位汇编要注意,虽然操作了32位寄存器,但实际上整个QWORD都会被影响。
不允许直接访问高32位,除非自己用位运算
写64位汇编
在x86-64架构(也称为AMD64或x64架构)中,寄存器可以根据操作的需求访问其不同部分。
例如访问RAX寄存器的某些特定部分:可以有RAX EAX AX AH AL
那么同理,R8~R15有(以R12为例子):64位: R12
32位: R12D
16位: R12W
8位: R12B
用VS的自带的64位编译链接器
ml64.exe:
- 功能:这是Microsoft的64位汇编程序(assembler),用于将汇编语言代码(通常是.asm文件)转换为机器代码或目标代码(object code)。
- 使用场景:当开发者编写了64位汇编代码,需要将其编译为目标文件(.obj文件)时,就会使用
ml64.exe
。它支持各种汇编语言的指令和宏,可以生成适用于64位Windows平台的目标代码。
link.exe:
- 功能:这是Microsoft的链接器(linker),用于将一个或多个目标文件(.obj文件)以及所需的库文件(.lib文件)链接成最终的可执行文件(.exe文件)或动态链接库(.dll文件)。
- 使用场景:在整个编译过程中,经过编译器和汇编器处理后生成的目标文件需要通过链接器将这些文件组合在一起,并解析外部引用和符号,生成最终的可执行程序。链接器还负责地址重定位、符号解析和优化等任务。
但是我们不直接在这个目录使用这俩,因为麻烦,我们采用VS自己的cmd
例如现在编译链接这个汇编
1 2 3 4 5 6
| .code start proc mov r8,r9 ret start endp end
|
需要的指令是:
1 2 3 4
| ml64 -c Hello.asm link /ENTRY:start /SUBSYSTEM:CONSOLE Hello.obj
link /ENTRY:WinMainCRTStartup /SUBSYSTEM:Windows Hello.obj /out:out.exe
|
另外值得一提的是,64位程序里面,不支持例如invoke,.if 等伪指令
64位程序调用约定:
- 64 位汇编与32位汇编的最大区别:就是函数调用方式的变化。x64 体系结构利用机会清除了现有 Win32 调用约定(如 __stdcall、__cdecl、__fastcall、_thiscall 等)的混乱。在设计调用约定时,因为现在寄存器增加,所以在 x64 中为了提升效率,我们选择多用寄存器传参。
- 减少调用约定行为还为可调试性带来了好处。需要了解的有关 x64 调用约定的主要内容是:它与 x86 fastcall 约定的相似之处。使用 x64 约定,会将前 4 个整数参数(从左至右)传入指定的 64 位寄存器:
四寄存器fastcall调用约定(类似于fastcall)
1. 参数传递
前四个整数或指针类型的参数通过以下四个寄存器传递:
- RCX: 第一个参数
- RDX: 第二个参数
- R8: 第三个参数
- R9: 第四个参数
浮点类型参数通过以下寄存器传递:
- XMM0: 第一个浮点参数
- XMM1: 第二个浮点参数
- XMM2: 第三个浮点参数
- XMM3: 第四个浮点参数
前 4 个以外的整数参数将使用栈传参。该指针被视为整数参数,因此始终位于 RCX 寄存器内。当传参需要用到栈的时候(代表参数大于 4 个了),考虑到不定参变参函数的情况,统一由函数的调用者负责平栈。据观察,我们常用的函数,它们的参数大多数都是小于等于 4 个的,比如 MessageBox()。在我们自己写的程序中,如果想让程序效率最优化,要让函数的参数少于等于 4 个。
浮点参数:前 4 个参数依序传入 XMM0、XMM2、XMM4、XMM4,后续的浮点参数将放置到线程堆栈上。
更进一步探究调用约定,即使参数可以传入寄存器,编译器仍然可以通过消耗 RSP 寄存器在堆栈上为其预留空间。至少,每个函数必须在堆栈上预留 32 个字节(4 个 64 位值)。该空间允许将传入函数的寄存器轻松地复制到已知的堆栈位置。不要求被调用函数将输入寄存器参数溢出至堆栈,但需要时,堆栈空间预留确保它可以这样做。当然,如果要传递 4 个以上的整数参数,则必须预留相应的额外堆栈空间。
2. 返回值
- 整数或指针类型的返回值通过 RAX 寄存器返回。
- 浮点类型的返回值通过 XMM0 寄存器返回。
3. 调用者和被调用者的责任
- 调用者保存(Caller-Saved)寄存器:
- RCX, RDX, R8, R9, RAX, XMM0-XMM5:调用者在调用函数前保存这些寄存器的值,因为函数调用后这些寄存器的值可能被改变。
尝试写一个弹窗的64位汇编:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| includelib kernel32.lib ;包含库目录 includelib user32.lib extern MessageBoxA:proc ;函数声明 extern ExitProcess:proc NULL EQU 0 MB_OK EQU 0 .const TITLE1 db "51asm",0 msg db "hello world",0 .code start proc ;64位函数调用约定,即四寄存器fastcall调用约定 mov rcx,NULL mov rdx,offset msg mov r8,offset TITLE1 MOV r9,0 call MessageBoxA mov rcx, 0 ; ExitProcess参数 call ExitProcess start endp end
|
但是这样的写法会运行不起来,会报一个0xC0000005,EXCEPTION_ACCESS_VIOLATION:
仔细看其实是因为没有平栈的缘故
movaps这个指令是需要栈对齐的
movaps
指令
movaps
是一种在 x86 和 x86-64 架构中使用的 SSE(Streaming SIMD Extensions)指令,用于在 XMM 寄存器和内存之间传输对齐的 128 位数据。
在 64 位系统中(如 x86-64 架构),函数调用约定规定栈指针(RSP)在调用函数前必须是 16 字节对齐的。这是为了确保使用 SIMD 指令(如 movaps
)时数据对齐,从而避免潜在的对齐错误和性能问题。
所以我们在start函数入口加一个add rsp,8或者sub rsp,8
1 2 3 4 5 6 7 8 9 10
| start proc ;64位函数调用约定,即四寄存器fastcall调用约定 mov rcx,NULL mov rdx,offset msg mov r8,offset TITLE1 MOV r9,0 call MessageBoxA mov rcx, 0 ; ExitProcess参数 call ExitProcess start endp
|
但是这样写真的没有问题吗?
我们发现进入一个函数是需要抬栈的,因为在64位程序中,使用四寄存器fastcall调用约定,所以会把寄存器的值存入栈中
如果不进行抬栈,就可能会把返回地址覆盖
每个函数都是如此,都得自己抬栈,哪怕是API也是如此:
那么问题来了,如果是五个参数呢?我们只有四个寄存器:RCX,RDX,R8,R9作为参数,那么接下来就是靠栈了。
但是值得一提的是,由于影子堆栈的存在,所以我们不好直接用push,也正是因为这个,所以我们看64位程序很少用push,比较常见的使用mov [rsp+xxx],xxx
大概结构长这样
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| start proc sub rsp,38h ;影子空间,预留栈空间,抬参数最多的函数需要的最大空间数 mov rax,7 mov qword ptr[rsp+20h],rax ;在这里传参 lea rcx, buffer ; 输出缓冲区 lea rdx, symbol ; 格式化字符串 "%s%s%d" lea r8, m_data ; 第一个字符串 "ccc" lea r9, msg ; 第二个字符串 "hello world" call wsprintfA
;64位函数调用约定,即四寄存器fastcall调用约定 mov rcx,NULL mov rdx,offset buffer mov r8,offset TITLE1 MOV r9,0 call MessageBoxA
mov rcx, 0 ; ExitProcess参数 call ExitProcess start endp
|
关于为什么64位程序似乎比较少使用RBP
1.栈帧优化
现代编译器通常会进行栈帧优化,直接使用 RSP
(栈指针寄存器)来访问局部变量和函数参数,而不需要 RBP
作为帧指针。这种优化称为“帧指针省略” (Frame Pointer Omission, FPO)。通过直接基于 RSP
的偏移量访问栈上的数据,可以节省一个寄存器,并减少函数调用的指令数量,从而提高程序性能。
2.栈对齐要求
由于 64 位架构对栈对齐有严格要求,尤其是在调用使用 SIMD 指令的函数时,栈必须是 16 字节对齐的。使用 RSP
直接管理栈帧可以更容易确保对齐要求,而不需要额外的调整。
64位程序不支持内联汇编
因为64位程序复杂,容易崩…….这理由好吧,但是可以用联合编译,就是用link把c++编译的obj和汇编写的obj链接在一起
联合编译:
其实也不难
先用汇编把需要的函数用64位汇编写好了,然后就生成OBJ,放在VS工程目录下,记住都得是64位的
然会就可以直接跑了,这也算是一个内联的方法吧
调用资源
如何想使用例如资源,STL函数,全局变量等,那么link的时候指定入口点就不应该在WinMain了,而是应该手动指定为WinMainCRTStartup
先查询一下WinMainCRTStartup属于哪一个lib,我们打开Nopad++进行查找
这样就可以找到WinMainCRTStartup这个函数是在libcmt.lib
直接在代码里面指定 includelib libcmt.lib可以吗???
答案必然是不行的,因为user32.lib是在系统路径里面有的,但是libcmt.lib是不在系统路径的,所以我们要指定路径才可以
1
| includelib "D:\\Microsoft Visual Studio\\2019\\Community\\VC\\Tools\\MSVC\\14.29.30133\\lib\\onecore\\x64\\libcmt.lib"
|
或者直接打开Visual Studio自带的cmd窗口,直接加一句 libcmt.lib 即可
1
| link /ENTRY:WinMainCRTStartup /SUBSYSTEM:Windows Hello.obj win_test.res /out:out.exe
|
[rsp+8][rsp+0x20]是影子空间,随后抬栈后,新的[rsp+0][rsp+0x20]是当前的函数空间
64位找main函数
动态调试的话没有IDA那种识别符号的功能,所以如果加壳或者上了奇奇怪怪的对抗之后,学会找main函数也是一个非常重要的技能
入口函数WinMainCRTStart mainCRTStartup (DBG条件下)
在程序链接的时候,有多个选项:
一种是dll,一种是静态库。那么一种入口代码是在dll,一种是在静态库
链接方式有四种
/MT (Release Static lib)
/MD (Release Dll)
/MDd (Debug Dll)
/MTd (Debug Static lib)
Debug版
通过观察源码我们可以发现一个规律:
首先这个是调用堆栈,
我们发现在调用完invoke_main以后,紧接着就调用了exit进行退出
事实上不止VS的编译器是这样,其他编译器也是如此
所以我们可以通过exit去定位 invoke_main函数
然后在invoke_main里面找到了有着三个参数的main函数,这便是寻找main函数的通用方法
Release版
如果是Release版本的话,通过追踪 _cexit 可以定位到main函数,但是Release版看了一下貌似没有exit,而是调用 _cexit 函数
总之貌似找_exit会比较稳一点,exit貌似调用的地方有点多???
IDA如何知道这是main函数呢
我们发现,程序在调用main函数的时候,会去调用三个函数,分别获取main函数的三个参数
可以看到的是,这个函数是粉色的,说明是动态链接进来的,也就是Dll。所以IDA就是通过识别到这个函数,推测就是main函数
做个实验,如果是静态链接的话:
发现已经识别不出main了。
解决办法:既然是静态库,那就可以尝试做sig文件,不过一般的IDA都已经做好了,这也不必太担心
识别技巧
结构体和类的区别
一般只有在有虚表的时候才能看出区别,还有就是再多一个this指针
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| class Temp { public: int m_age; void setage(int age) { m_age = age; } }; int main(int arg) { Temp temp; temp.setage(10); return 0; }
|
能靠this指针去识别是否为成员函数(this指针会赋值给rcx寄存器)
构造函数的识别
识别构造析构函数还是老一套,构造函数的特征就是:
1.传this指针的函数第一个被调用的
2.返回this指针的
3.有初始化虚表的(如果有虚表)
这样一般就可以识别出构造函数
类对象数组的识别:
很好识别,直接看有没有调用构造迭代器和析构迭代器即可
虚函数的识别:
例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91
| #include <iostream> using namespace std; class A { public: int A1; int A2; int A3; A() { cout << "this is A's constructor" << endl; } ~A() { cout << "this is A's destructor" << endl; } virtual void func1() { cout << "this is A's func1" << endl; } virtual void func2() { cout << "this is A's func2" << endl; } virtual void func3() { cout << "this is A's func3" << endl; } };
class B { public: int B1; int B2; int B3; B() { cout << "this is B's constructor" << endl; } virtual void func1() { cout << "this is B's func1" << endl; }
virtual void func2() { cout << "this is B's func2" << endl; }
virtual void func3() { cout << "this is B's func3" << endl; }
~B() { cout << "this is B's destructor" << endl; } };
class C:public A,public B { public: int C1; int C2; C() { cout << "this is C's constructor" << endl; } virtual void Cfunc() { cout << "this is C's Cfunc" << endl; } ~C() { cout << "this is C's destructor" << endl; }
};
int main() { C* ptr = new C; ptr->Cfunc(); ptr->A::func1(); ptr->B::func1(); delete ptr; return 0; }
|
main函数反编译如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75
| .text:0000000140012BC0 ; __unwind { // j___CxxFrameHandler4_0 .text:0000000140012BC0 40 55 push rbp .text:0000000140012BC2 57 push rdi .text:0000000140012BC3 48 81 EC 68 01 00 00 sub rsp, 168h .text:0000000140012BCA 48 8D 6C 24 20 lea rbp, [rsp+20h] .text:0000000140012BCF 48 8D 0D 53 24 01 00 lea rcx, __344EA88D_code@cpp ; JMC_flag .text:0000000140012BD6 E8 D0 E8 FF FF call j___CheckForDebuggerJustMyCode .text:0000000140012BD6 .text:0000000140012BDB B9 38 00 00 00 mov ecx, 38h ; '8' ; size .text:0000000140012BE0 E8 6B E4 FF FF call j_??2@YAPEAX_K@Z ; operator new(unsigned __int64) .text:0000000140012BE0 .text:0000000140012BE5 48 89 85 08 01 00 00 mov [rbp+150h+var_48], rax .text:0000000140012BEC 48 83 BD 08 01 00 00 00 cmp [rbp+150h+var_48], 0 .text:0000000140012BF4 74 15 jz short loc_140012C0B .text:0000000140012BF4 .text:0000000140012BF6 48 8B 8D 08 01 00 00 mov rcx, [rbp+150h+var_48] ; this .text:0000000140012BFD E8 88 E6 FF FF call j_??0C@@QEAA@XZ ; C::C(void) .text:0000000140012BFD .text:0000000140012C02 48 89 85 38 01 00 00 mov [rbp+150h+var_18], rax .text:0000000140012C09 EB 0B jmp short loc_140012C16 .text:0000000140012C09 .text:0000000140012C0B ; --------------------------------------------------------------------------- .text:0000000140012C0B .text:0000000140012C0B loc_140012C0B: ; CODE XREF: main+34↑j .text:0000000140012C0B 48 C7 85 38 01 00 00 00 00 00+mov [rbp+150h+var_18], 0 .text:0000000140012C0B 00 .text:0000000140012C0B .text:0000000140012C16 .text:0000000140012C16 loc_140012C16: ; CODE XREF: main+49↑j .text:0000000140012C16 48 8B 85 38 01 00 00 mov rax, [rbp+150h+var_18] .text:0000000140012C1D 48 89 85 E8 00 00 00 mov [rbp+150h+var_68], rax .text:0000000140012C24 48 8B 85 E8 00 00 00 mov rax, [rbp+150h+var_68] .text:0000000140012C2B 48 89 45 08 mov [rbp+150h+var_148], rax .text:0000000140012C2F 48 8B 45 08 mov rax, [rbp+150h+var_148] .text:0000000140012C33 48 8B 00 mov rax, [rax] .text:0000000140012C36 48 8B 4D 08 mov rcx, [rbp+150h+var_148] .text:0000000140012C3A FF 50 18 call qword ptr [rax+18h] .text:0000000140012C3A .text:0000000140012C3D 48 8B 4D 08 mov rcx, [rbp+150h+var_148] ; this .text:0000000140012C41 E8 F3 E6 FF FF call j_?func1@A@@UEAAXXZ ; A::func1(void) .text:0000000140012C41 .text:0000000140012C46 48 8B 45 08 mov rax, [rbp+150h+var_148] .text:0000000140012C4A 48 83 C0 18 add rax, 18h .text:0000000140012C4E 48 8B C8 mov rcx, rax ; this .text:0000000140012C51 E8 48 E6 FF FF call j_?func1@B@@UEAAXXZ ; B::func1(void) .text:0000000140012C51 .text:0000000140012C56 48 8B 45 08 mov rax, [rbp+150h+var_148] .text:0000000140012C5A 48 89 85 28 01 00 00 mov [rbp+150h+var_28], rax .text:0000000140012C61 48 83 BD 28 01 00 00 00 cmp [rbp+150h+var_28], 0 .text:0000000140012C69 74 1A jz short loc_140012C85 .text:0000000140012C69 .text:0000000140012C6B BA 01 00 00 00 mov edx, 1 ; unsigned int .text:0000000140012C70 48 8B 8D 28 01 00 00 mov rcx, [rbp+150h+var_28] ; this .text:0000000140012C77 E8 E7 E4 FF FF call j_??_GC@@QEAAPEAXI@Z ; C::`scalar deleting destructor'(uint) .text:0000000140012C77 .text:0000000140012C7C 48 89 85 38 01 00 00 mov [rbp+150h+var_18], rax .text:0000000140012C83 EB 0B jmp short loc_140012C90 .text:0000000140012C83 .text:0000000140012C85 ; --------------------------------------------------------------------------- .text:0000000140012C85 .text:0000000140012C85 loc_140012C85: ; CODE XREF: main+A9↑j .text:0000000140012C85 48 C7 85 38 01 00 00 00 00 00+mov [rbp+150h+var_18], 0 .text:0000000140012C85 00 .text:0000000140012C85 .text:0000000140012C90 .text:0000000140012C90 loc_140012C90: ; CODE XREF: main+C3↑j .text:0000000140012C90 33 C0 xor eax, eax .text:0000000140012C92 48 8D A5 48 01 00 00 lea rsp, [rbp+148h] .text:0000000140012C99 5F pop rdi .text:0000000140012C9A 5D pop rbp .text:0000000140012C9B C3 retn .text:0000000140012C9B ; } // starts at 140012BC0 .text:0000000140012C9B .text:0000000140012C9B main endp .text:0000000140012C9B
|
定位构造函数,首先要找到this指针。很淦的是,一般函数调用也会使用rcx,这给我们识别是否为类函数还是普通函数造成影响。
所以我们最好先找到类对象的构造函数,这样就可以找到this指针了
但是局限性是,其实找构造函数并没有什么很多特征,但是一旦存在虚表,其实就很好识别出构造函数和析构函数。
例如我们点开这个函数 call j_??0C@@QEAA@XZ:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| .text:00000001400120F0 ; __unwind { // j___CxxFrameHandler4_0 .text:00000001400120F0 48 89 4C 24 08 mov [rsp-8+this], rcx .text:00000001400120F5 55 push rbp .text:00000001400120F6 57 push rdi .text:00000001400120F7 48 81 EC E8 00 00 00 sub rsp, 0E8h .text:00000001400120FE 48 8D 6C 24 20 lea rbp, [rsp+20h] .text:0000000140012103 48 8D 0D 1F 2F 01 00 lea rcx, __344EA88D_code@cpp ; JMC_flag .text:000000014001210A E8 9C F3 FF FF call j___CheckForDebuggerJustMyCode .text:000000014001210A .text:000000014001210F 48 8B 8D E0 00 00 00 mov rcx, [rbp+0D0h+this] ; this .text:0000000140012116 E8 FF F2 FF FF call j_??0A@@QEAA@XZ ; 这里先调基类的构造函数,填虚表 .text:0000000140012116 .text:000000014001211B 90 nop .text:000000014001211C 48 8B 85 E0 00 00 00 mov rax, [rbp+0D0h+this] .text:0000000140012123 48 83 C0 18 add rax, 18h .text:0000000140012127 48 8B C8 mov rcx, rax ; this .text:000000014001212A E8 31 F3 FF FF call j_??0B@@QEAA@XZ ; B::B(void) .text:000000014001212A .text:000000014001212F 90 nop .text:0000000140012130 48 8B 85 E0 00 00 00 mov rax, [rbp+0D0h+this] .text:0000000140012137 48 8D 0D 52 9C 00 00 lea rcx, ??_7C@@6BA@@@ ; const C::`vftable'{for `A'} .text:000000014001213E 48 89 08 mov [rax], rcx .text:0000000140012141 48 8B 85 E0 00 00 00 mov rax, [rbp+0D0h+this] .text:0000000140012148 48 8D 0D 71 9C 00 00 lea rcx, ??_7C@@6BB@@@ ; const C::`vftable'{for `B'} .text:000000014001214F 48 89 48 18 mov [rax+18h], rcx .text:0000000140012153 48 8D 15 86 9C 00 00 lea rdx, aThisIsCSConstr ; "this is C's constructor" .text:000000014001215A 48 8B 0D 67 10 01 00 mov rcx, cs:__imp_?cout@std@@3V?$basic_ostream@DU?$char_traits@D@std@@@1@A ; _Ostr .text:0000000140012161 E8 3F EF FF FF call j_??$?6U?$char_traits@D@std@@@std@@YAAEAV?$basic_ostream@DU?$char_traits@D@std@@@0@AEAV10@PEBD@Z ; std::operator<<<std::char_traits<char>>(std::ostream &,char const *) .text:0000000140012161 .text:0000000140012166 48 8D 15 D4 EE FF FF lea rdx, j_??$endl@DU?$char_traits@D@std@@@std@@YAAEAV?$basic_ostream@DU?$char_traits@D@std@@@0@AEAV10@@Z ; std::endl<char,std::char_traits<char>>(std::ostream &) .text:000000014001216D 48 8B C8 mov rcx, rax .text:0000000140012170 FF 15 EA 0F 01 00 call cs:__imp_??6?$basic_ostream@DU?$char_traits@D@std@@@std@@QEAAAEAV01@P6AAEAV01@AEAV01@@Z@Z ; std::ostream::operator<<(std::ostream & (*)(std::ostream &)) .text:0000000140012170 .text:0000000140012176 90 nop .text:0000000140012177 48 8B 85 E0 00 00 00 mov rax, [rbp+0D0h+this] .text:000000014001217E 48 8D A5 C8 00 00 00 lea rsp, [rbp+0C8h] .text:0000000140012185 5F pop rdi .text:0000000140012186 5D pop rbp .text:0000000140012187 C3 retn .text:0000000140012187 ; } // starts at 1400120F0
|
发现有填虚表:这一看就是构造函数了,因此她的参数就是this指针,于是我们就靠这个去还原类和类函数
而且上面这个是一个有多重继承的关系,C继承于A B
很明显这里先进行A的构造,再进行B的构造,最后才进行C类的构造。会有虚表的覆盖,定位C类的析构函数也很简单,直接对C类的虚表进行查询引用即可