本帖最后由 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](https://610-pic-bed.oss-cn-shenzhen.aliyuncs.com/image-20210301164228804.png)
#include "stdafx.h"
void __declspec (naked) function(){ __asm{ ret } } int main(int argc, char* argv[]) { function(); return 0; }
接下来进入反汇编的世界 函数外部![image-20210301164802880](https://610-pic-bed.oss-cn-shenzhen.aliyuncs.com/image-20210301164802880.png)
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](https://610-pic-bed.oss-cn-shenzhen.aliyuncs.com/image-20210301165007211.png)
6: void __declspec (naked) function(){ 00401030 ret
开始分析00401078 call @ILT+10(function) (0040100f)
我们可以发现裸函数的调用和普通函数的调用并没有什么区别,都是call 函数地址
但接着进入函数内部就可以看到 00401030 ret
函数的内部有且仅有我们代码中用__asm所写的ret语句,没有任何其余的代码
执行完ret语句后,函数正常返回,接下来就和普通的空函数没有区别了 ![image-20210301165714965](https://610-pic-bed.oss-cn-shenzhen.aliyuncs.com/image-20210301165714965.png)
得出结论于是我们可以知道,裸函数的内部汇编代码完全由我们自己来实现,之所以要写上一个ret也是为了让函数能够正常地返回 再来看看不添加ret语句时的反汇编代码情况 我们将裸函数内部的__asm删除 void __declspec (naked) function(){
}
然后再观察函数内部的汇编代码 ![image-20210301170117241](https://610-pic-bed.oss-cn-shenzhen.aliyuncs.com/image-20210301170117241.png)
我们可以看到函数内部为一堆 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](https://610-pic-bed.oss-cn-shenzhen.aliyuncs.com/image-20210301173020417.png)
我们可以发现函数返回后和普通函数并无差异 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](https://610-pic-bed.oss-cn-shenzhen.aliyuncs.com/image-20210301184443302.png)
函数内部![image-20210301184511944](https://610-pic-bed.oss-cn-shenzhen.aliyuncs.com/image-20210301184511944.png)
我们这里主要是关注三个地方:参数压栈、函数返回值、返回后执行语句 参数压栈:先push 2再 push 1 函数返回值:ret 返回后执行语句:add esp,8 __stdcall函数外部![image-20210301185612216](https://610-pic-bed.oss-cn-shenzhen.aliyuncs.com/image-20210301185612216.png)
函数内部![image-20210301185804974](https://610-pic-bed.oss-cn-shenzhen.aliyuncs.com/image-20210301185804974.png)
接着关注三个地方:参数压栈、函数返回值、返回后执行语句 参数压栈:先push 2再 push 1 函数返回值:ret 8 返回后执行语句:xor eax,eax __fastcall函数外部![image-20210301190140046](https://610-pic-bed.oss-cn-shenzhen.aliyuncs.com/image-20210301190140046.png)
函数内部![image-20210301190331254](https://610-pic-bed.oss-cn-shenzhen.aliyuncs.com/image-20210301190331254.png)
依旧关注三个地方:参数压栈、函数返回值、返回后执行语句 参数压栈:先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,自己在函数内实现了堆栈内平衡
|