对于C语言来说,对于一个变量,取地址&和取值*的结果肯定是不一样的,但是对于函数来说,由于编译器做了限制,因此对函数来说&和*操作的值是和原来一样的。

void test() {
    printf("hello\r\n");
}

int main()
{
    printf("%x %x %x)\r\n", test, &test, *test);
}
//输出 eb0c11c7,eb0c11c7,eb0c11c7

        从上可以看出,函数名其实就是函数的开始地址,不管对这个地址如何操作其值都不变。

        另外,对于函数指针变量来说,情况是这样的:

int main()
{   
    void (*pfunc)() = test;
    printf("%x %x %x\r\n", pfunc, &pfunc, *pfunc);
}
// 输出 6e1011c7,4f9bf938,6e1011c7

        可以看出pfunc和*pfunc的值是一样的,&pfunc的值却不一样,实际上也是由于编译器对函数地址做了保护。对函数的开始地址取值,就会取到汇编代码,如果修改里面的代码就可以改变程序运行的逻辑。这一般也是不被允许的,因为该处的内存是写保护的,实际可以通过一些手段去修改,下面会讲。

        对于C++来说,类成员函数其实和普通函数一样,地址是固定的,跟类对象无关,使用 &Class::member 的方式获取地址,也是由于编译器做了限制,从而我们只能获取public成员函数的地址:

class A {
public:
    void test() {
        printf("hello\r\n");
    }
};
int main()
{   
    printf("%x\r\n", &A::test);
}
//输出 4f9e1136

        因此如果成员函数中没有调用成员变量,就可以跟调用普通函数一样直接对地址进行函数调用,并不用使用类对象调用。但是如果成员函数中使用了类成员变量的,就行不通了,因此类成员变量是根据类对象生成的,而且传参的时候类成员对象会把this指针进行传递。

class A {
public:
    void test() {
        printf("hello");
        test2();
    }
protected:
    void test2() {
        printf("world\r\n");
    }
};
int main()
{   
    void(A:: * pFunc)() = &A::test; //地址赋值给变量
    void* p = *(void**)&pFunc;  //强制转换成void*
    void(*pfun1)() = (void(*)())p; //再转成void()类型的函数
    pfun1(); //调用函数
}
//输出 hello world

        从上面所知,函数名是一个地址,我们再详细研究下这个地址,先看一段代码:

void test() {
    printf("---1----");
}
void test2() {
    printf("---2----");
}
printf("0x%x, 0x%x\r\n", test, test2);

00D75262 68 CF 13 D7 00       push        offset test2 (0D713CFh)  
00D75267 68 C5 13 D7 00       push        offset test (0D713C5h) 

        从上面代码得知,test 指向地址 0x0D713C5 ,test2 指向 0x0D713C5,找到这2个地址,看看里面存放着什么:

00D713C5 E9 96 12 00 00       jmp         test (0D72660h)  

00D713CF E9 2C 04 00 00       jmp         test2 (0D71800h)

        可以看出这里存放着5个字节,第一个字节是E9,其实是JMP的汇编指令, 后面是一个int类型的数值,0x1296和0x42c。这个int类型的数值是什么意思呢,下面分析一下:

        test 函数的实际所在地址是在 0x0D72660 处, 而 0x00D713C5 + 0x1296 + 5字节正好等于 0x0D72660,因此,0x1296存储的是对于当前地址+5字节代码后的的偏移值。

        那么,我们是不是可以修改 test 地址里的值,让他指向 test2 的地址,从而在调用 test 的时候反而去执行 test2 函数呢? 我们来试试,因此写了如下这段代码:

    unsigned char* p1 = (unsigned char*)(void*)test;
    unsigned char* p2 = (unsigned char*)(void*)test2;

    int val1 = *((int*)(p1 + 1)); //test的偏移(第二个字节开始)
    int val2 = *((int*)(p2 + 1)); //test2的偏移
    //计算偏移差
    int offset = (unsigned int)(p2)-(unsigned int)(p1);
    val2 += offset; //test2的偏移+差值就是需要填在test地址中的数值
    //赋值
    *(int*)(p1 + 1) = val2;
    test();//调用test函数

        编译通过,但是运行的时候抛出了异常,赋值的时候不允许操作:

微信截图_20220608175458.png

        这就是上面说的操作系统对该地址的保护,我们可以调用函数来修改内存的写保护,windows提供了 VirtualProtect 函数来修改内存保护选项,函数的使用方法请参考MSDN。然后我们修改p1所指向的内存后,代码变成如下:

    unsigned char* p1 = (unsigned char*)(void*)test;
    unsigned char* p2 = (unsigned char*)(void*)test2;

    int val1 = *((int*)(p1 + 1)); //test的偏移(第二个字节开始)
    int val2 = *((int*)(p2 + 1)); //test2的偏移
    //计算偏移差
    int offset = (unsigned int)(p2)-(unsigned int)(p1);
    val2 += offset; //test2的偏移+差值就是需要填在test地址中的数值
    
    DWORD newp = 0x40, oldp = 0;
    VirtualProtect(p1, 5, newp, &oldp);//内存去保护
    //赋值
    *(int*)(p1 + 1) = val2;
    VirtualProtect(p1, 5, oldp, &newp);//恢复保护

    test();//调用test函数
    
    ////输出 ---2----

         最后运行代码,发现输出了test2 函数中的内容,证明我们的想法是正确的,这其实就是最简单的对函数的hook,是不是有点意思呢?😄

2022-06-08 18:09:22lazypos:也可以对类成员函数进行hook,方法跟上面差不多,请尝试一下吧~
显示更多
Copyright © 2018-2023 lazypos.cn