什么?对象,虚?
PE 文件格式
为什么会有这个?逆向为什么没有这个!
微软官方文档:PE 格式 - Win32 apps
使用 010editor 查看。需要先下载插件:模板->模板库->安装 exe.bt 。alt+4 可以查看PE文件格式
DOS 头 两个重要的变量 e_magic
(判断是否为DOS头,固定为5a4d
) 和 e_lfanew
(NT头的偏移)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 typedef struct _IMAGE_DOS_HEADER { WORD e_magic; WORD e_cblp; WORD e_cp; WORD e_crlc; WORD e_cparhdr; WORD e_minalloc; WORD e_maxalloc; WORD e_ss; WORD e_sp; WORD e_csum; WORD e_ip; WORD e_cs; WORD e_lfarlc; WORD e_ovno; WORD e_res[4 ]; WORD e_oemid; WORD e_oeminfo; WORD e_res2[10 ]; LONG e_lfanew; } IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER
DOS stub MS-DOS 存根是在 MS-DOS 下运行的有效应用程序 。 它放置在 EXE 映像的前面。 链接器在此处放置一个默认存根,当映像在 MS-DOS 中运行时,该存根输出消息“此程序无法在 DOS 模式下运行”。
NT 头 IMAGE_NT_HEADERS
结构
1 2 3 4 5 typedef struct _IMAGE_NT_HEADERS64 { DWORD Signature; IMAGE_FILE_HEADER FileHeader; IMAGE_OPTIONAL_HEADER64 OptionalHeader; } IMAGE_NT_HEADERS64, *PIMAGE_NT_HEADERS64;
签名 签名是一个固定的值
1 #define IMAGE_NT_SIGNATURE 0x00004550
文件头包含了很多重要的属性
1 2 3 4 5 6 7 8 9 typedef struct _IMAGE_FILE_HEADER { WORD Machine; WORD NumberOfSections; DWORD TimeDateStamp; DWORD PointerToSymbolTable; DWORD NumberOfSymbols; WORD SizeOfOptionalHeader; WORD Characteristics; } IMAGE_FILE_HEADER, * PIMAGE_FILE_HEADER;
可选头
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 typedef struct _IMAGE_OPTIONAL_HEADER64 { WORD Magic; BYTE MajorLinkerVersion; BYTE MinorLinkerVersion; DWORD SizeOfCode; DWORD SizeOfInitializedData; DWORD SizeOfUninitializedData; DWORD AddressOfEntryPoint; DWORD BaseOfCode; ULONGLONG ImageBase; DWORD SectionAlignment; DWORD FileAlignment; WORD MajorOperatingSystemVersion; WORD MinorOperatingSystemVersion; WORD MajorImageVersion; WORD MinorImageVersion; WORD MajorSubsystemVersion; WORD MinorSubsystemVersion; DWORD Win32VersionValue; DWORD SizeOfImage; DWORD SizeOfHeaders; DWORD CheckSum; WORD Subsystem; WORD DllCharacteristics; ULONGLONG SizeOfStackReserve; ULONGLONG SizeOfStackCommit; ULONGLONG SizeOfHeapReserve; ULONGLONG SizeOfHeapCommit; DWORD LoaderFlags; DWORD NumberOfRvaAndSizes; IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]; } IMAGE_OPTIONAL_HEADER64, * PIMAGE_OPTIONAL_HEADER64;
ImageBase: 文件在内存中的首选装入地址。如果有可能(也就是说,目前如果没有其他占据这块地址,它是正确对齐的并且是一个合法的地址,等等),加载器试图在这个地址装入PE文件。如果可执行文件是在这个地址装入的,那么加载器将跳过应用基址重定位 的步骤。
Section 节的初始化数据由简单的字节块组成。 但是,对于全部为零的节,不需要包含节数据。
每个节的数据位于节头中的 PointerToRawData 字段给出的文件偏移量处。 文件中此数据的大小由 SizeOfRawData 字段指示。 如果 SizeOfRawData 小于 VirtualSize,则其余部分用零填充。
这里的对齐和可选头的 SectionAlignment
和 FileAlignment
相关
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 typedef struct _IMAGE_SECTION_HEADER { BYTE Name[IMAGE_SIZEOF_SHORT_NAME]; union { DWORD PhysicalAddress; DWORD VirtualSize; } Misc; DWORD VirtualAddress; DWORD SizeOfRawData; DWORD PointerToRawData; DWORD PointerToRelocations; DWORD PointerToLinenumbers; WORD NumberOfRelocations; WORD NumberOfLinenumbers; DWORD Characteristics; } IMAGE_SECTION_HEADER, * PIMAGE_SECTION_HEADER;
HelloWorld 非常简单的源码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 #include <iostream> int func (int a, int b, int c, int d, int e, int f, int g) { return a; } int main () { int a; std::cout << "hello world" << std::endl; a = 10 ; std::cout << func (a, 2 , 3 , 4 , 5 , 6 , 7 ) << std::endl; return 0 ; }
带符号表的反汇编,可以看出其比较长的 namespace 和 函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 __int64 __fastcall main () { std::ostream *v0; unsigned int v1; __int64 v2; j___CheckForDebuggerJustMyCode (&_4C11826D_reverse_cpp); v0 = std::operator <<<std::char_traits<char >>(std::cout, "hello world" ); std::ostream::operator <<(v0, std::endl<char ,std::char_traits<char >>); v1 = func (10 , 2 , 3 , 4 , 5 , 6 , 7 ); v2 = std::ostream::operator <<(std::cout, v1); std::ostream::operator <<(v2, std::endl<char ,std::char_traits<char >>); return 0 i64; }
CALL 方式 具体参考:自变量传递和命名约定 | Microsoft Learn ****
__cdecl
是 C 和 C++ 程序的默认调用约定。 堆栈由caller
清理,函数传递顺序 从右到左
__stdcall
调用约定用于调用 Win32 API 函数。callee
清理堆栈,函数传递方式 从右到左。
1 2 #define WINAPI __stdcall void __stdcall method () ;
__fastcall
调用约定指定尽可能在寄存器中传递函数的自变量。 此调用约定仅适用于 x86 体系结构。参数传递
x86 从左到右的顺序找到前两个 DWORD
或更小自变量将在 ECX 和 EDX 寄存器中传递;所有其他自变量在堆栈上从右向左传递。
x64 下是 rcx, rdx, r8, r9
__thiscall
的调用约定用于 x86 体系结构上的 C++ 类成员函数,特定于 Microsoft
比如说 call func 的 汇编代码,我在x64下是 fastcall
1 2 3 4 5 6 7 8 9 .text:0000000140011 CEE mov [rbp+0F 0h+a], 0 Ah .text:0000000140011 CF5 mov [rsp+130 h+g], 7 ; g .text:0000000140011 CFD mov [rsp+130 h+f], 6 ; f .text:0000000140011 D05 mov [rsp+130 h+e], 5 ; e .text:0000000140011 D0D mov r9d, 4 ; d .text:0000000140011 D13 mov r8d, 3 ; c .text:0000000140011 D19 mov edx, 2 ; b .text:0000000140011 D1E mov ecx, [rbp+0F 0h+a] ; a .text:0000000140011 D21 call j_?func@@YAHHHHHHHH@Z ; func (int ,int ,int ,int ,int ,int ,int )
但是在 x86 就是 cdecl call
1 2 3 4 5 6 7 8 9 .text:0041253 C push 7 ; g .text:0041253 E push 6 ; f .text:00412540 push 5 ; e .text:00412542 push 4 ; d .text:00412544 push 3 ; c .text:00412546 push 2 ; b .text:00412548 mov eax, [ebp+a] .text:0041254B push eax ; a .text:0041254 C call j_?func@@YAHHHHHHHH@Z ; func (int ,int ,int ,int ,int ,int ,int )
但是不变的是 rax
为函数返回值
Class 测试代码
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 #include <iostream> #include <vector> class Person {private : int _age; public : Person () { _age = 0 ; std::cout << "person" << std::endl; } virtual ~Person () { std::cout << "~person" << std::endl; } virtual int GetAge () = 0 ; virtual void SetAge (int age) = 0 ; }; class Boy : public Person {public : Boy () { std::cout << "boy" << std::endl; } virtual ~Boy () { std::cout << "~boy" << std::endl; } int GetAge () { std::cout << "boy getage" << std::endl; return 114 ; } void SetAge (int age) { std::cout << "boy setage" << std::endl; } }; int main () { std::vector<Boy> boys; Boy b; boys.push_back (b); auto f = [](int a, int b) -> int { std::cout << "lambda" << std::endl; return a + b; }; std::cout << f (1 , 2 ) << std::endl; return EXIT_SUCCESS; }
C++ 名称粉碎机制:举个例子 std::cout
名字是 __imp_?cout@std@@3V?$basic_ostream@DU?$char_traits@D@std@@@1@A:qword
。
MSVC
反编译出来,优化比较严重,因此使用g++
方法 析构函数和构造函数
构造函数调用:函数声明时;析构函数:生命周期结束时
反汇编时,方法第一个参数一般是 this
指针
继承 在构造时,构造顺序为先构造父类,后构造子类。
1 2 3 4 5 6 7 8 9 void __fastcall Boy::Boy (Boy *this ) { __int64 v1; Person::Person (this ); *(_QWORD *)this = off_4CC8; v1 = std::operator <<<std::char_traits<char >>(&std::cout, "boy" ); std::ostream::operator <<(v1, &std::endl<char ,std::char_traits<char >>); }
在析构时,析构顺序为先析构子类,后析构父类。
1 2 3 4 5 6 7 8 9 void __fastcall Boy::~Boy (Boy *this ){ __int64 v1; *(_QWORD *)this = off_4CC8; v1 = std::operator <<<std::char_traits<char >>(&std::cout, "~boy" ); std::ostream::operator <<(v1, &std::endl<char ,std::char_traits<char >>); Person::~Person (this ); }
虚函数 虚函数也是函数,体现了CPP的多态性。
根据对象的实际类型来调用成员函数,而不是根据指针的类型来对该块内存进行解释并调用属于该指针类型所属的成员函数,简化代码实现多态性
一个很好的解释:父类声明子类Set<String> m = new HashMap<String>
,但是调用方法是子类的
在cpp
中使用一个指针和一个表格来进行实现的。这个表格称为虚表vftable
,这个指针称为虚表指针。
如果一个类中有虚函数,编译器则会在对象的首部加入虚表指针,以存放所属于该对象的虚表
虚表指针的填充时机:只有在构造函数和析构函数中会对虚表指针重新赋值,其他位置没有对该指针的操作 。
其在构造函数和析构函数给vfptr
赋值
1 2 3 4 5 6 7 .data.rel.ro:0000000000004 CC8 off_4CC8 dq offset _ZN3BoyD2Ev ; DATA XREF: Boy::Boy (void )+1 D↑o .data.rel.ro:0000000000004 CC8 ; Boy::~Boy ()+10 ↑o ... .data.rel.ro:0000000000004 CC8 ; Boy::~Boy () .data.rel.ro:0000000000004 CD0 dq offset _ZN3BoyD0Ev ; Boy::~Boy () .data.rel.ro:0000000000004 CD8 dq offset _ZN3Boy6GetAgeEv ; Boy::GetAge (void ) .data.rel.ro:0000000000004 CE0 dq offset _ZN3Boy6SetAgeEi ; Boy::SetAge (int ) .data.rel.ro:0000000000004 CE8 public _ZTV6Person ; weak
析构函数为什么也要重新给虚表赋值呢?
编译器无法预知这个子类以后是否会被其他类继承,如果被继承,就成了父类,在执行析构函数时会先执行当前对象的析构函数,然后向祖父类的方向按继承线路逐层调用各类析构函数,当前对象的析构函数开始执行时,其虚表也是当前对象的,所以执行到父类的析构函数时,虚表必须改写为父类的虚表
引用与指针 指针
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 #include <iostream> int main () { int a = 100 ; printf ("a=%d\n" , a); int *p = &a; *p = 200 ; printf ("a=%d\n" , a); return 0 ; } push rbp mov rbp, rsp sub rsp, 20 h mov rax, fs:28 h mov [rbp+var_8], rax xor eax, eaxmov [rbp+var_14], 64 h ; 'd' mov eax, [rbp+var_14] mov esi, eax lea rax, format ; "a=%d\n" mov rdi, rax ; format mov eax, 0 call _printf lea rax, [rbp+var_14] mov [rbp+var_10], rax ; *p mov rax, [rbp+var_10] mov dword ptr [rax], 0 C8h mov eax, [rbp+var_14] mov esi, eax lea rax, format ; "a=%d\n" mov rdi, rax ; format mov eax, 0 call _printf
左值引用
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 #include <iostream> int main () { int a = 100 ; printf ("a=%d\n" , a); int &p = a; p = 200 ; printf ("a=%d\n" , a); return 0 ; } push rbp mov rbp, rsp sub rsp, 20 h mov rax, fs:28 h mov [rbp+var_8], rax xor eax, eaxmov [rbp+var_14], 64 h ; 'd' mov eax, [rbp+var_14] mov esi, eax lea rax, format ; "a=%d\n" mov rdi, rax ; format mov eax, 0 call _printf lea rax, [rbp+var_14] mov [rbp+var_10], rax mov rax, [rbp+var_10] mov dword ptr [rax], 0 C8h mov eax, [rbp+var_14] mov esi, eax lea rax, format ; "a=%d\n" mov rdi, rax ; format mov eax, 0 call _printf
指针和左值引用:在汇编看起来一模一样,引用也是为了满足C++特性,比如操作符重载
右值引用???
lambda 表达式 比较简单的lambda 可以看出来其参数类型与个数
1 2 3 4 5 6 7 8 9 10 v3 = main::{lambda (int ,int )#1 }::operator ()(&v6, 1LL , 2LL ); __int64 __fastcall main::{lambda (int ,int )#1 }::operator ()(__int64 a1, int a2, int a3) { __int64 v3; v3 = std::operator <<<std::char_traits<char >>(&std::cout, "lambda" ); std::ostream::operator <<(v3, &std::endl<char ,std::char_traits<char >>); return (unsigned int )(a2 + a3); }
debugger WinDbg 命令:Common WinDbg Commands x64dbg 文档: x64dbg documentation
windbg 常见命令,感觉 preview 更加好看
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 bp: 断点 bl: list bc: clear d{a|b|c|d|D|f|p|q|u|w|W} [Options] [Range] db: dump byte dw: word dd: dword dq: qword dt: type g: go t: trace,步入 p: 步过 r: register k: dump stack .frame s: search e: edit
某些命令
1 2 3 4 5 .echo .reboot .reload .cls: 清屏幕 .help
x64dbg 操作起来比较容易,可以使用鼠标和右键😋。除了某些命令