吾爱破解 - LCG - LSG |安卓破解|病毒分析|www.52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 6424|回复: 21
收起左侧

[调试逆向] 逆向基础笔记九 C语言内联汇编和调用协定

  [复制链接]
lyl610abc 发表于 2021-3-1 20:49
本帖最后由 lyl610abc 于 2021-3-12 16:34 编辑

继续更新个人的学习笔记,
其它笔记传送门
逆向基础笔记一 进制篇
逆向基础笔记二 数据宽度和逻辑运算
逆向基础笔记三 通用寄存器和内存读写
逆向基础笔记四 堆栈篇
逆向基础笔记五 标志寄存器
逆向基础笔记六 汇编跳转和比较指令
逆向基础笔记七 堆栈图(重点)
逆向基础笔记八 反汇编分析C语言
逆向基础笔记十 汇编寻找C程序入口
逆向基础笔记十一 汇编C语言基本类型
逆向基础笔记十二 汇编 全局和局部 变量
逆向基础笔记十三 汇编C语言类型转换
逆向基础笔记十四 汇编嵌套if else
逆向基础笔记十五 汇编比较三种循环
逆向基础笔记十六 汇编一维数组
逆向基础笔记十七 汇编二维数组 位移 乘法
逆向基础笔记十八 汇编 结构体和内存对齐
逆向基础笔记十九 汇编switch比较if else
逆向基础笔记二十 汇编 指针(一)
逆向基础笔记二十一 汇编 指针(二)
逆向基础笔记二十二 汇编 指针(三)
逆向基础笔记二十三 汇编 指针(四)
逆向基础笔记二十四 汇编 指针(五) 系列完结

C语言内联汇编和调用协定

前面我们通过分析反汇编分析了C语言,现在我们来探究如何在C语言里直接自写汇编函数

这里就要引入C语言中裸函数的概念

裸函数

声明裸函数

裸函数与普通函数的区别在于在函数前多声明了

__declspec (naked)

以此来表面该函数是一个裸函数

裸函数作用

要讲裸函数的作用,就不得不提到裸函数与普通函数的区别

裸函数与普通函数区别

前面反汇编C语言的笔记里,我们可以得知一个普通空函数的反汇编代码并不少,保护现场、恢复现场等等都有,那么这些反汇编代码是如何产生的呢?

答案是:编译器

编译器会为我们产生这些反汇编代码

相比之下,只要普通函数加上裸函数前缀转化为裸函数,编译器就会知道这个函数无需额外生成上面所说的保护现场、恢复现场等反汇编代码,函数执行所需的反汇编代码由我们自己来实现

于是裸函数的作用呼之欲出:

当我们不希望编译器为我们生成函数里的汇编代码,而是想要自己实现函数内部的汇编代码时,就可以使用裸函数来告诉编译器不要去额外生成汇编代码

最简单的裸函数

void __declspec (naked) function(){
        __asm{
                ret
        }
}

上面是一个最简单的裸函数,反汇编代码只有一行ret

与普通空函数相比,同样是什么都没做,但却要加上ret,为什么?

我们可以来看看这个最简单的裸函数的执行流程,并注意与普通空函数对比

分析最简单的裸函数

完整代码

先给出完整的代码

image-20210301164228804

#include "stdafx.h"

void __declspec (naked) function(){
        __asm{
                ret
        }
}
int main(int argc, char* argv[])
{
        function();
        return 0;
}

接下来进入反汇编的世界

函数外部

image-20210301164802880

13:       function();
00401078   call        @ILT+10(function) (0040100f)
14:       return 0;
0040107D   xor         eax,eax
15:   }
0040107F   pop         edi
00401080   pop         esi
00401081   pop         ebx
00401082   add         esp,40h
00401085   cmp         ebp,esp
00401087   call        __chkesp (004010e0)
0040108C   mov         esp,ebp
0040108E   pop         ebp
0040108F   ret

函数内部

image-20210301165007211

6:    void __declspec (naked) function(){
00401030   ret

开始分析

00401078   call        @ILT+10(function) (0040100f)

我们可以发现裸函数的调用和普通函数的调用并没有什么区别,都是call 函数地址


但接着进入函数内部就可以看到

00401030   ret

函数的内部有且仅有我们代码中用__asm所写的ret语句,没有任何其余的代码


执行完ret语句后,函数正常返回,接下来就和普通的空函数没有区别了

image-20210301165714965

得出结论

于是我们可以知道,裸函数的内部汇编代码完全由我们自己来实现,之所以要写上一个ret也是为了让函数能够正常地返回

再来看看不添加ret语句时的反汇编代码情况

我们将裸函数内部的__asm删除

void __declspec (naked) function(){

}

然后再观察函数内部的汇编代码

image-20210301170117241

我们可以看到函数内部为一堆 int 3,也就是CC即初始化堆栈的内容

程序执行到这里就会产生中断,无法正常执行,所以我们才要加上一条汇编ret语句,让函数能够正常执行返回


大致了解了裸函数后,我们就来使用内联汇编自己实现加法函数

内联汇编实现加法函数

自写加法函数

#include "stdafx.h"

int __declspec (naked) Plus(int x,int y){
                __asm{
                //保留调用前堆栈
                push ebp
                //提升堆栈
                mov ebp,esp
                sub esp,0x40
                //保护现场
                push ebx
                push esi
                push edi
                //初始化提升的堆栈,填充缓冲区
                mov eax,0xCCCCCCCC
                mov ecx,0x10
                lea edi,dword ptr ds:[ebp-0x40]
                rep stosd
                //函数核心功能

                //取出参数
                mov 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[])
{
        Plus(1,2);
        return 0;
}

不难发现,其实我们自己实现的加法函数就是模拟了编译器为我们做的事情,此时进到函数内部也会看到

函数内部

6:    int __declspec (naked) Plus(int x,int y){
00401030   push        ebp
7:        __asm{
8:            //保留调用前堆栈
9:            push ebp
10:           //提升堆栈
11:           mov ebp,esp
00401031   mov         ebp,esp
12:           sub esp,0x40
00401033   sub         esp,40h
13:           //保护现场
14:           push ebx
00401036   push        ebx
15:           push esi
00401037   push        esi
16:           push edi
00401038   push        edi
17:           //初始化提升的堆栈,填充缓冲区
18:           mov eax,0xCCCCCCCC
00401039   mov         eax,0CCCCCCCCh
19:           mov ecx,0x10
0040103E   mov         ecx,10h
20:           lea edi,dword ptr ds:[ebp-0x40]
00401043   lea         edi,ds:[ebp-40h]
21:           rep stosd
00401047   rep stos    dword ptr [edi]
22:           //函数核心功能
23:
24:           //取出参数
25:           mov eax,dword ptr ds:[ebp+8]
00401049   mov         eax,dword ptr ds:[ebp+8]
26:           //参数相加
27:           add eax,dword ptr ds:[ebp+0xC]
0040104D   add         eax,dword ptr ds:[ebp+0Ch]
28:
29:
30:           //恢复现场
31:           pop edi
00401051   pop         edi
32:           pop esi
00401052   pop         esi
33:           pop esi
00401053   pop         esi
34:
35:           //降低堆栈
36:           mov esp,ebp
00401054   mov         esp,ebp
37:           pop ebp
00401056   pop         ebp
38:
39:           //返回
40:           ret

执行的就是我们自己所写的代码,而非编译器所生成的,并且也能够实现加法函数的功能

函数返回后

image-20210301173020417

我们可以发现函数返回后和普通函数并无差异

00401081   add         esp,8

都有这一行平衡堆栈的语句,也就是堆栈外平衡,但如果我们想要在函数内部就平衡堆栈,也就是实现堆栈内平衡,也就是希望函数返回后没有这个外部的堆栈平衡语句,让堆栈的平衡工作由我们自己来处理,该如何做到?

这里就要引入C语言的调用协定这个概念了

调用协定

常见的几种调用协定:

调用协定 参数压栈顺序 平衡堆栈
__cdecl 从右至左入栈 调用者清理栈
__stdcall 从右至左入栈 自身清理堆栈
__fastcall ECX/EDX传送前两个  剩下:从右至左入栈 自身清理堆栈

其中__cdecl为C语言默认调用协定

接下来我们来比较一下这三种调用协定

int __cdecl Plus1(int x,int y){
        return x+y;
}
int __stdcall Plus2(int x,int y){
        return x+y;
}
int __fastcall Plus3(int x,int y){
        return x+y;
}

同样都是一个简单的加法函数,分别采用了三种不同的调用协定,我们来用汇编来一察他们的区别

__cdecl

首先是我们最熟悉的__cdecl协定,和我们上一个笔记分析的简单加法函数不无区别

观察反汇编:

函数外部

image-20210301184443302

函数内部

image-20210301184511944


我们这里主要是关注三个地方:参数压栈、函数返回值、返回后执行语句

参数压栈:先push 2再  push 1

函数返回值:ret

返回后执行语句:add esp,8

__stdcall

函数外部

image-20210301185612216

函数内部

image-20210301185804974

接着关注三个地方:参数压栈、函数返回值、返回后执行语句

参数压栈:先push 2再  push 1

函数返回值:ret 8

返回后执行语句:xor eax,eax

__fastcall

函数外部

image-20210301190140046

函数内部

image-20210301190331254

依旧关注三个地方:参数压栈、函数返回值、返回后执行语句

参数压栈:先move edx,2再mov ecx,1

函数返回值:ret

返回后执行语句:xor eax,eax

对比三种协定

__cdecl __stdcall __fastcall
参数压栈 push 2  push1 push2 push1 mov edx,2     mov ecx,1
函数返回值 ret ret 8 ret
返回后执行语句 add esp,8 xor eax,eax xor eax,eax

我们可以得出结论:

__cdecl是将参数压入栈中,然后在函数执行返回后再平衡堆栈,也就是堆栈外平衡

__stdcall也是将参数压入栈中,但是是在函数内部通过ret xxx来平衡堆栈,也就是堆栈内平衡

__fastcall则是在参数个数小于等于2时直接使用edx和ecx作为参数传递的载体,没用使用到堆栈,自然也就无须平衡堆栈,但是当参数个数大于2时,则多出来的那几个参数则按stdcall的方式来处理,也是采用堆栈内平衡

接下来再谈谈__stdcall中返回值的问题

我们可以看到,我们在上面的加法函数中push了两个立即数2和1,返回值是8

这是不是意味着ret xxxx中xxxx=参数个数*4?

并不是!!!这里ret xxxx里的xxxx和压入参数的数据宽度有关

我们这里压入的两个立即数的数据宽度都是4个字节=32bit,因此我们这里是ret 4+4=8

如果改成push ax,也就是压入2个字节=16bit时则应该ret 2

这里可以参考逆向基础笔记四 堆栈篇中堆栈相关汇编指令的push指令


了解了以上调用协定后,我们就可以修改之前的简单加法裸函数,将其改为堆栈内平衡

堆栈内平衡加法函数

__declspec (naked) __stdcall int  Plus(int x,int y){
                __asm{
                //保留调用前堆栈
                push ebp
                //提升堆栈
                mov ebp,esp
                sub esp,0x40
                //保护现场
                push ebx
                push esi
                push edi
                //初始化提升的堆栈,填充缓冲区
                mov eax,0xCCCCCCCC
                mov ecx,0x10
                lea edi,dword ptr ds:[ebp-0x40]
                rep stosd
                //函数核心功能

                //取出参数
                mov 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 8
        }        
}
int main(int argc, char* argv[])
{
        Plus(1,2);
        return 0;
}

与前面相比,修改ret 为ret 8,自己在函数内实现了堆栈内平衡

免费评分

参与人数 5吾爱币 +7 热心值 +5 收起 理由
kwdwkiss + 1 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
bailemenmlbj + 1 + 1 谢谢@Thanks!
红尘书生 + 1 + 1 热心回复!
SilverBulletY + 1 + 1 我很赞同!
朱朱你堕落了 + 3 + 1 很好,很重要的基础知识。

查看全部评分

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

 楼主| lyl610abc 发表于 2021-3-9 01:43
kwdwkiss 发表于 2021-3-8 23:39
我目前用win10的mingw-w64的gcc和g++来编译的,貌似裸函数会报错。
请问楼主是用什么?看上去像vc6.0,好 ...

我是直接用XP虚拟机(论坛的)安装的VC6.0,挺久之前下载的了,你在论坛找找看,貌似VC6也是在论坛下的
kwdwkiss 发表于 2021-3-8 23:39
我目前用win10的mingw-w64的gcc和g++来编译的,貌似裸函数会报错。
请问楼主是用什么?看上去像vc6.0,好像win10安装需要设置一些东西?
有地址吗?
ET_52 发表于 2021-3-1 21:42
dedegoodboy 发表于 2021-3-1 22:42
谢谢分享,学习下
nur11111 发表于 2021-3-1 23:04
感谢楼主的无私奉献,学习一下
秋叶秋 发表于 2021-3-2 04:36
感谢楼主的无私奉献,学习一下
penggang123 发表于 2021-3-2 09:24
大佬厉害
SilverBulletY 发表于 2021-3-2 10:13
谢谢分享
pxwhsh 发表于 2021-3-2 11:24
总结的很好。。。赞扬一个。。
zhrmghgtw 发表于 2021-3-2 15:18
谢谢分享
deweyhent 发表于 2021-3-3 20:09
非常好的教程,谢谢分享
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则 警告:本版块禁止灌水或回复与主题无关内容,违者重罚!

快速回复 收藏帖子 返回列表 搜索

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - LCG - LSG ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2024-4-27 09:55

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表