大神论坛

找回密码
快速注册
查看: 723 | 回复: 0

[语言编程类] 大神论坛 逆向脱壳分析基础学习笔记九 C语言内联汇编和...

主题

帖子

5

积分

初入江湖

UID
20
积分
5
精华
威望
10 点
违规
大神币
68 枚
注册时间
2021-03-14 10:40
发表于 2021-03-14 17:03
本帖最后由 kay2kay 于 2021-03-14 17:03 编辑

本文为本人的滴水逆向破解脱壳学习笔记之一,为本人对以往所学的回顾和总结,可能会有谬误之处,欢迎大家指出。
陆续将不断有笔记放出,希望能对想要入门的萌新有所帮助,一起进步


所有笔记链接:

大神论坛 逆向脱壳分析基础学习笔记一 进制篇
大神论坛 逆向脱壳分析基础学习笔记二 数据宽度和逻辑运算
大神论坛 逆向脱壳分析基础学习笔记三 通用寄存器和内存读写
大神论坛 逆向脱壳分析基础学习笔记四 堆栈篇
大神论坛 逆向脱壳分析基础学习笔记五 标志寄存器 
大神论坛 逆向脱壳分析基础学习笔记六 汇编跳转和比较指令
大神论坛 逆向脱壳分析基础学习笔记七 堆栈图(重点)(需登录才能访问)
大神论坛 逆向脱壳分析基础学习笔记八 反汇编分析C语言
大神论坛 逆向脱壳分析基础学习笔记九 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从右至左入栈自身清理堆栈
__fastcallECX/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  push1push2 push1mov edx,2     mov ecx,1
函数返回值retret 8ret
返回后执行语句add esp,8xor eax,eaxxor 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,自己在函数内实现了堆栈内平衡

返回顶部