Windows 逆向-函数的本质

Oyst3r 于 2023-12-01 发布

前言

好好学这节,后面有个逆向 C 语言的小项目,好好学了才能自己逆出来

课堂

裸函数的定义

我们知道,如果现在在 C 语言里面去定义一个函数,即使什么都不写,我们的编译器也会给我们生成很多的东西,会有基本的传参,提升堆栈,恢复堆栈什么的,比如说下面这样

那有没有什么办法可以让它不生成这么多东西呢?当然可以,这就请出了我们今天的主角—裸函数,咱这么来定义它—void __declspec(naked) 函数名(){},然后再跳到刚刚设置的断点,看看啥子情况吧

可以发现什么都没有了

现在来重新定义一下这个名词:裸函数定义好调用时,编译器和链接器不会帮我们做任何事情,想做什么由我们自己决定,裸函数调用时编译器和链接器会在 main 中生成一个 call 指令,所以裸函数调用还是会将下一行指令地址压入堆栈,将 EIP 的值改为 call 后面跟的地址;然后再执行会跳到一个 jmp 指令(中转、跳板);再执行程序会跳到 jmp 后面的地址开始执行,但是由于是裸函数,会发现这些地址后面跟的都是 int 3,表示断点的意思。所以程序运行到这里时一定会停下来,而且裸函数不像正常的空函数一样有 retn 返回指令,即裸函数执行后无法结束,即回不去了。那么此时就会报错
为解决上述问题:如果此时在裸函数中自己手动添加一段返回的汇编指令,那么会发现此时在裸函数中就已经有一条 retn 返回的汇编指令了,此时裸函数执行完后会回到 call 指令的下一跳指令继续执行,即不会报错了。(所以定义裸函数最简单也一定要加一句 retn 指令,不然就会报错)

void __declspec(naked) Test(){

    __asm{      //__asm{}是在c语言中添加汇编指令的固定格式
        retn
    }
}

OKK 看到这里,我们即使只有一个裸函数,那么通过前面的学习,也能去成功的编写出我们自己想要的代码了

裸函数的分类

调用约定 参数压栈顺序 平衡堆栈
__cdecl 从右至左入栈 外平栈
__stdcall 从右至左入栈 内平栈
__fastcall 前两个存入 ECX/EDX,剩下:从右至左入栈 不用平衡堆栈/内平栈

__cdecl调用约定

C 和 C++默认使用__cdecl这种调用约定,传参时使用堆栈(内存)来传递,且是从最后一个参数往前依次 push 入栈;且堆栈平衡是在调用函数(call)的外面平衡,即在 call 指令后面有一条add esp,x指令来平衡堆栈,又称为外平栈

//__cdecl
int __cdecl Plus1(int x,int y){
    return x + y;
}

void main(int argc, char* argv[]){
    Plus1();
}

__stdcall调用约定

win32 操作系统的 API 函数使用的是__stdcall 这种调用约定,传参的规则和__cdecl 一样,但是平衡堆栈不一样:你如果去分析操作系统自带的 API 函数,那么在函数调用 call 指令的下面一行见不到 add esp,x 这条平衡堆栈的指令,因为使用__stdcall 这种调用约定的函数,都用的是内平栈,即函数返回时有 retn x 指令,retn 后面跟一个立即数,表示修改 eip 的值为返回地址并且将 esp 加立即数,所以此时才函数调用的内部就平衡堆栈了

//__stdcall
int __stdcall Plus2(int x,int y){
    return x + y;
}

void main(int argc, char* argv[]){
    Plus2();
}

这下子就没了 add esp,8

但是在函数里面会在 ret 的时候就多加 8

__fastcall调用预定

__fastcall调用约定,如果参数数量少时,传递参数使用的是寄存器(快!);而且使用寄存器存储参数,调用完后是不需要平衡堆栈的!

优势:如果某一个函数需要不停地调用且参数数量少,那么此函数使用__fastcall 效率会高很多,因为使用寄存器存参数且不需要平衡堆栈。如果参数数量大于 2,那么还是按照从后往前存参数,后面的参数还会会用 push 存入堆栈,只是最前面的两个参数使用 mov 寄存器来存储;那么最后还是要平衡堆栈,只是平衡 add esp,x 中的 x 不需要那么大了。

//__fastcall
int __fastcall Plus4(int x,int y,int a,int b){
    return x + y + a + b;
}

void main(int argc, char* argv[]){
    Plus4(1,2,3,4);
}

这个是函数内部,可以发现最后还是要使 esp+8 的

判断函数参数个数的方法

经过上面的说明,我们在逆向的时候,再看到 call 的时候,就不能 1 单单的只靠 retn 后面的数来判断函数到底有几个参数!如果是外平栈也不能根据 add esp 后面跟的数来判断!因为参数可能用了 push 存入堆栈,还可能用了 mov 存入寄存区。最后只需要根据存入内存中的数据占得内存宽度来平衡堆栈,而用 mov 存入寄存器的参数那一部分不需要平衡(因为没有影响内存)

最好的办法就是结合全体看,可以先看看 call 前面的,再看看 call 函数里面做了什么,尤其看 edx,ecx,ebp+8 等等,再看看 ret,下面给个例子

00401050   push        ebp                //1.只找给别人赋值的寄存器,即寄存器做为mov后面的参数
00401051   mov         ebp,esp
00401053   sub         esp,48h
00401056   push        ebx
00401057   push        esi
00401058   push        edi
00401059   push        ecx
0040105A   lea         edi,[ebp-48h]     //这里有一个,但是ebp中存的值是在函数内赋的值
0040105D   mov         ecx,12h
00401062   mov         eax,0CCCCCCCCh
00401067   rep stos    dword ptr [edi]
00401069   pop         ecx
0040106A   mov         dword ptr [ebp-8],edx  //这里有一个,而且发现函数中没有给edx赋值的语句
0040106D   mov         dword ptr [ebp-4],ecx  //这里有一个,而且发现函数中没有给ecx赋值的语句
00401070   mov         eax,dword ptr [ebp-4]
00401073   add         eax,dword ptr [ebp-8]
00401076   add         eax,dword ptr [ebp+8]  //这里有一个[ebp+8]有些可疑,结合下面的ret指令判断
00401079   mov         [g_x (00427958)],eax      //修改全局变量
0040107E   pop         edi
0040107F   pop         esi
00401080   pop         ebx
00401081   mov         esp,ebp
00401083   pop         ebp
00401084   ret         4               //上面两个寄存器,再结合这里的4,可以基本确定函数有3个参数

作业

// luohanshu.cpp : Defines the entry point for the console application.
//

#include "stdafx.h"
#include "../Include/TCHAR.H"
#include "../Include/STRING.H"
#include "../Include/STDIO.H"

int __declspec(naked) plus(int x ,int y ,int z)
{
    __asm
    {
        push ebp
            mov ebp,esp
            sub esp,0x40
            push ebx
            push esi
            push edi
            lea edi,dword ptr ds:[ebp-0x40]
            mov eax,0xCCCCCCCC
            mov ecx,0x10
            rep stosd
            mov dword ptr ds:[ebp-4],2
            mov dword ptr ds:[ebp-8],3
            mov dword ptr ds:[ebp-0xC],4

            mov eax,dword ptr ds:[ebp+8]
            add eax,dword ptr ds:[ebp+0xC]
            add eax,dword ptr ds:[ebp+0x10]
            add eax,dword ptr ds:[ebp-4]
            add eax,dword ptr ds:[ebp-8]
            add eax,dword ptr ds:[ebp-0xC]

            pop edi
            pop esi
            pop ebx
            mov esp,ebp
            pop ebp

            ret

    }
}

int main(int argc, char* argv[])
{
    int d = plus(1,2,3);
    printf("%d",d);

    return 0;
}

OKK,继续淦哇