C++逆向分析——this指针

Posted 将者,智、信、仁、勇、严也。

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++逆向分析——this指针相关的知识,希望对你有一定的参考价值。

this指针

概述

C++是对C的拓展,C原有的语法C++都支持,并在此基础上拓展了一些语法:封装、继承、多态、模板等等。C++拓展新的语法是为了让使用更加方便、高效,这样就需要编译器多做了很多事情,接下来我们就需要一一学习这些概念。

封装

之前我们学习过结构体这个概念,那么结构体可以做参数传递吗?我们来看一下如下代码:

struct Student
int a;
int b;
int c;
int d;
;
 
int Plus(Student s)
return s.a + s.b + s.c + s.d;
 
void main()
Student s = 1, 2, 3, 4;
int res = Plus(s);
return;

上面这段代码是定义一个结构体,然后将该结构体传入Plus函数(将结构体成员相加返回),那么问题来了,结构体它是否跟数组一样,传递的是指针呢?来看一下反汇编代码:

可以很清晰的看见,结构体作为参数传递时栈顶(ESP)提升了0x10(16个字节,也就是结构体的四个成员【int】的宽度),而后将ESP的值给了EAX,再通过EAX(ESP)将结构体的成员传入函数,结构体成员从左到右依次从栈顶向下复制进入堆栈。

也就是说当我们将结构体作为参数传递时与我们传整数什么的是没有本质区别的,唯一的区别就是传递结构体时不是使用的push来传递的,而是一次性的提升堆栈,然后mov赋值。

虽然我们可以使用结构体进行传参,但是这也存在一个问题,就是当我们使用结构体传参时,假设结构体有40个成员,那么就存在着大量的内存被复制,这样效率很低,是不推荐使用的

那如果非要这样使用该怎么办呢?我们可以使用指针传递的方式来,修改一下代码:

struct Student
int a;
int b;
int c;
int d;
;
 
int Plus(Student* p)
return p->a + p->b + p->c + p->d;
 
void main()
Student s = 1, 2, 3, 4;
int res = Plus(&s);
return;

这样我们就可以使用指针的方式来避免内存的重复使用,效率更高。

可能很多人看到这就很疑惑了,那这跟C++有什么关系呢?我们之前说过C++和C的本质区别,就是编译器替代我们做了很多事情;别着急,慢慢来看。

我们使用指针优化过的代码,实际上还是存在小缺陷的,当结构体成员很多的时候,我们在Plus函数体内就要用指针的调用方式,一堆成员相加...

那么是否可以让我们调用更加简单,更加方便呢?如下代码就可以:

struct Student
int a;
int b;
int c;
int d;
 
int Plus()
return a + b + c + d;
;
 
void main()
Student s = 1, 2, 3, 4;
int res = s.Plus();
return;

将函数放在结构体内,就不需要我们再去写传参、再去使用指针的调用方式了,因为这些工作编译器帮我们完成了,而本质上这与指针调用没有区别:

而这种写法就是C++的概念:封装;也就是说将函数写在结构体内的形式就称之为封装,其带来的好处就是我们可以更加方便的使用结构体的成员。

讲到了封装,我们就要知道另外两个概念:

  1. :带有函数的结构体,称为类;

  2. 成员函数:结构体里的函数,称为成员函数

    1. 函数本身不占用结构体的空间(函数不属于结构体

    2. 调用成员函数的方法与调用结构体成员的语法是一样的 → 结构体名称.函数名()

this指针

之前我们学过了封装,如下代码:

struct Student
int a;
int b;
int c;
int d;
 
int Plus()
return a + b + c + d;
;
 
void main()
Student s = 1, 2, 3, 4;
int res = s.Plus();
return;

其对应的反汇编代码如下:

可以看见我们使用s.Plus()的时候,传递的参数是一个指针,这个指针就是当前结构体的地址,这个指针就是this指针。(通常情况下编译器会使用ecx来传递当前结构体的指针)

那么当我们将Plus函数修改成无返回值,不调用结构体成员后,这个指针还会传递过来么?

struct Student
int a;
int b;
int c;
int d;
 
void Plus()
 
;
 
void main()
Student s = 1, 2, 3, 4;
s.Plus();
return;

我们看下反汇编代码,发现指针依然会传递过来:

那也就是说this指针是编译器默认传入的,通常会通过ecx进行参数的传递,不管你用还是不用,它都存在着

既然this指针会作为参数传递,我们是否也可以直接使用这个指针呢?答案是可以的:

struct Student
int a;
int b;
 
void Init(int a, int b)
this->a = a;
this->b = b;
 
;

我们在结构体的成员函数内使用this这个关键词就可以调用了,如上代码所示。

那么this指针有什么作用呢?我们可以看下如下代码:

struct Student
int a;
int b;
 
void Init(int a, int b)
a = a;
b = b;
 
;
 
void main()
Student s;
s.Init(1,2);
return;

这段代码我们要实现的就是,使用成员函数初始化成员的值,但是实际运行却不符合我们的预期:

跟进反汇编代码发现,这里就是将传入的参数赋值给了参数本身,并没有改变成员的值,这是因为编译器根本不知道你这里的a到底是谁,所以我们就需要借助this指针来实现:

#include <stdio.h>
 
struct Student
int a;
int b;
 
void Init(int a, int b)
this->a = a;
this->b = b;
 
void Print()
printf("%d %d", this->a, this->b);
 
;
 
void main()
Student s;
s.Init(1,2);
s.Print();
return;

为了方便,添加一个成员函数,用于打印输出成员的值:

可以看见,这里成功进行初始化了。

总结:

  1. this指针是编译器默认传入的,通常会使用ecx进行参数的传递

  2. 成员函数都有this指针,无论是否使用

  3. this指针不能做++ --等运算,也不可以被重新赋值

  4. this指针不占用结构体的宽度

 

this指针和函数都不占用struct的空间,我们验证下:

#include <cstdio>

struct A 
    char* hello() 
        return "hi";
    
;

int main() 
    A a;
    printf("empty struct size=%d\\n", sizeof(a));

 

输出为1。

所以可以知道,没有任何成员变量的struct大小为1.

C++逆向总结

本篇文章适用于有一定C语言基础,但是对C++零基础的同学看,讲解了C++里的一些概念

C在定义一个结构体的时候,就是定义一个新的数据类型
而C++在定义一个结构体,会有一个this指针,指向本结构体的地址,传的this指针的值一般给到了ecx。主要应用如下

typedef struct

    int x = 1;
    int y = 2;
    void function(int x, int y)
    
        this->x = x;
        this->y = y;
    
str;
int main()

    str p;
    p.function(3,4);

这样可以修改结构体里的变量值
在C++中,如果有一些struct的东西是重复的,那我们就懒得再写,直接copy过来,c++提供一个很方便的东西继承

struct person

    int age;
    int sex;
;

struct A

    int age;
    int sex;
    char name;


struct A:person

    char name;

两种方式都差不多,person就是A的爹,称为父类,父类指针可以用来访问子类里父类的东西。
再说一点,如果子类和父类有定义相同的变量,那么需要在子类变量前加入一个类名加两个冒号才行
构造函数就是声明类后可以直接启动的函数,可以重载,可以有参数,无返回值

class test


    public:
    test(int x,int y)
    
        printf("%d\\n%d\\n",x,y);
    
;

int main()

    test base(2,3);

~   

析构函数,这个类不用后执行的一个函数,可以用来free,不能重载,不能有参数,无返回值
有父类子类,先父构造、子构造、子构析,父构析
虚函数表
如果在一个函数前加virtual,就是虚函数虚函数调用:

mov ecx,dword ptr [ebp-10]
mov edx,dword ptr [ecx];虚表地址给到edx
mov esi,esp
mov ecx,dword ptr [ebp-10]
call dword ptr [edx];取虚表地址里的数据,就是虚表

底层上从this指针处会多存一个4字节(32位),这个4字节就是存储着虚表的地址
这个虚表是一个数组,里面有函数地址
有一个父类的话,是直接把父类的虚表拿过来,子类接着写,如果子类和父类虚函数名重复,子类中copy来的父类虚表上的函数会被改写,这就是动态绑定。在这个基础上子类每多继承一个父类,就会多一个虚表,也就是多4字节(32位)而子类的虚函数放在第一张虚表了,其他虚表放其他父类的虚函数(不推荐使用)
所以取虚表:

class base
........
base b;
printf("虚表的地址是%d",*(int*)&b);

多态

#include<stdio.h>
class base

        public:
                int x;
                int y;
        public:
                base()
                
                        x = 1;
                        y = 2;
                
                virtual void print()
                
                        printf("base:%x %x\\n",x,y);
                
;
class sub1:public base

public:
        int A;
        sub1()
        
                x = 4;
                y = 5;
                A = 6;
        
        virtual void print()
        
                printf("sub1:%x %x %x\\n",x,y,A);
        

;
class sub2:public base

public:
        int b;
        sub2()
        
                x = 7;
                y = 8;
                b = 9;
        
        virtual void print()
        
                printf("sub2:%x %x %x",x,y,b);
        

;

void test()

        base b;
        sub1 s1;
        sub2 s2;

        base* arr[] = &b,&s1,&s2;
        for(int i = 0;i<3;i++)
        
                arr[i]->print();
        

int main()

        test();


上面的运行结果

base:1 2
base:4 5
base:7 8//前面没有加virtual

base:1 2//后面加了virtual
sub1:4 5 6
sub2:7 8 9

这样也指明了:析构函数最好定义成虚函数.

我们之前写了一个函数是这样传参的

func(int* a)

    *a = 10;
    //a = (int*)203020;

我们知道一般而言函数不能更改参数的值,假如传参进去是ebp+8,然后ebp+8给eax,这样我们在函数里修改的值是eax的值,不是参数的值,但我们观察一下上面的反汇编

*a = 10;
mov eax,dword ptr [ebp+8]
mov dword ptr [eax],0ah

可以看到,假如传进去的是个地址,直接把地址里的数据给改了,也就是能修改参数
所以C++里面有个引用符号

void Test(int &x)

    x = 1;
    //x = 2;

int main()

    Test(a);

在底层传参是一样的

lea eax,[ebp-4]
push eax
call@ILT+0(Test)

有什么不一样的地方,就是有一种情况指针的值被修改,比如上面的第一行注释的代码,就是指针乱指
如果用引用的话,不能够进行乱指,比如x = 2,只是普通地把2给到这个变量,编译器不允许引用指向别的地方,引用中那个x永远代表a
同样的数据类型给指针就是指向另一个,不管原来的,而引用就是把指向的那个东西的值修改一下
还有就是传结构体的话参数太多,直接传引用好一点

我们都知道class里面的成员,数据成员都是私有的

class Person

public:
    Person(int x,int y)
    
        this->x = x;
        this->y = y;
    
//friend void print(const Person& refPer);
;
void Print(Person &p)

    printf("%d %d\\n",p.x,p.y);

如果这样的话是不能打出来的,因为私有的,只有类型的其他成员能访问,比如自己的方法啥的,但是我们就要访问咋整(通过指针访问内存可以)
那我们可以把注释中的代码写出来,告诉这个类,这个print是它的朋友,通过这个函数访问类就可以访问私有成员了

在C++中,通常说我们要new一个对象,在对象不用的时候delete他。其实new是把malloc再封装一遍的,比malloc多了一层。比如int* pi = new int;就分配了int大小的空间,所以申请对象person* pa = new person(1,2);并执行构造函数,也就是说,new一个对象的目的其实就是把他放到堆里,不然的话我们只能放到全局变量(data )或者局部变量(stack)里了

要注意delete和new一定配套,如果new int[10]; 一定要delete[];

还有标准输入流与输出流可以看看,这里不再赘述,C++写很多东西要方便很多。可以尝试看看AFL源码和浏览器之类的

以上是关于C++逆向分析——this指针的主要内容,如果未能解决你的问题,请参考以下文章

C++逆向总结

C++逆向总结

C++类和对象(this指针6个默认成员函数const成员)

逆向分析技术

c++之类和对象——类的定义,存储方式,this指针!(五千字长文详解!)

C++反汇编与逆向分析技术揭秘的目录