0x1 准备工作

1.1、准备工具

  • IDA:交互式反汇编工具
  • OllyDbg:用户层调试工具
  • Visual Studio:微软开发工具

1.2、基础知识

  • C++开发

  • 汇编语言

0x2 查找真正的main()函数

入口点开始到Main()函数之间的代码都是编译器加进去用于初始化环境用的。

main()函数其实是有3个参数的,这取决于Windows系统的机制。

查找方法:

1、字符串搜索法

2、栈回溯法

3、逐步分析法

4、小例子

C源代码:

程序执行的时候会将路径保存在argv字符数组中,因此argc的值始终是等于1的。程序未经处理会显示”Helllo world“。

#include "stdafx.h"

int _tmain(int argc, _TCHAR* argv[])
{
if (argc)
{
printf("Hello world!\r\n");
}
else
{
printf("Hello everybody!\r\n");
} return 0;
}

反汇编后的代码:

ESP是栈指针,也就是当前栈的所在位置。+4就是在当前堆栈+4的地方取内容。

CMP指令是对两个操作数做减法操作,仅影响标志位。

00041000 >/$  55            push ebp
00041001 |. 8BEC mov ebp,esp
00041003 |. 837D 08 00 cmp [arg.1],0x0
00041007 |. 74 11 je X9-3.0004101A
00041009 |. 68 48640500 push 9-3.00056448 ; /Hello world!\r\n
0004100E |. E8 2D000000 call 9-3.printf ; \printf
00041013 |. 83C4 04 add esp,0x4 ; esp+4的意思就是在当前堆栈+4的地方取其内容
00041016 |. 33C0 xor eax,eax
00041018 |. 5D pop ebp
00041019 |. C3 retn
0004101A |> 68 58640500 push 9-3.00056458 ; /Hello everybody!\r\n
0004101F |. E8 1C000000 call 9-3.printf ; \printf
00041024 |. 83C4 04 add esp,0x4
00041027 |. 33C0 xor eax,eax
00041029 |. 5D pop ebp
0004102A \. C3 retn

0x3 函数识别初探

1、函数调用约定的参数入栈书序、回收堆栈的规则

  • C规范 _cdecl 参数入栈顺序从右到左 调用者负责回收堆栈
  • pascal规范 pascal 参数入站从左到右 被调用者负责回收堆栈
  • 快速调用规范 __fastcall 从右到左 被调用者负责回收堆栈
  • 标准调用规范 __stdcall 从右到左 被调用者负责回收堆栈

2、调用方式示例

调用约定 关键字 参数入栈顺序 回收堆栈
C规范 _cdecl 从右到左 调用者负责
Pascal规范 pascal 从左到右 被调用者负责
快速调用规范 _fastcall 从右到左,使用寄存器传参 被调用者负责
标准调用规范 _stdcall 从右到左 被调用者负责

示例程序:

#include "stdafx.h"

int __cdecl fun_a(int nNumA, int nNumB, int nNumC)	// C规范
{
return nNumA+nNumB+nNumC;
}
int __fastcall fun_b(int nNumA, int nNumB, int nNumC)// 快速调用
{
return nNumA+nNumB-nNumC;
}
int __stdcall fun_c(int nNumA, int nNumB, int nNumC) // 标准调用
{
return nNumA-nNumB-nNumC;
} int _tmain(int argc, _TCHAR* argv[])
{
printf("11111111111111111111111111111"); fun_a(argc, 1, 2); //func_a,入栈顺序从右向左,调用者平衡堆栈
fun_b(argc, 1, 2); //fun_b,入栈顺序从右向左,被调用者平衡堆栈
fun_c(argc, 1, 2);//fun_c,入栈顺序从右向左,被调用者平衡堆栈 return 0;
}

反汇编程序:

Main()函数

.text:004016A0 main__ proc near ; CODE XREF: j_main__j
.text:004016A0
................
.text:00401707 push 2 ; 参数3入栈
.text:00401709 push 1 ; 参数2入栈
.text:0040170B mov eax, [ebp+arg_0]
.text:0040170E push eax ; 参数1入栈
.text:0040170F call j_fun_a__ ; func_a,cdecl调用方式,入栈顺序从右向左,调用者平衡堆栈
.text:00401714 add esp, 0Ch ; 由Main()函数销毁fun_a用到的局部变量平衡堆栈
.text:00401714 ;
.text:00401717 push 2 ; 参数3入栈
.text:00401719 mov edx, 1 ; 参数2入栈
.text:0040171E mov ecx, [ebp+arg_0] ; 参数1传递给ecx
.text:00401721 call j_fun_b__ ; fun_b,fastcall调用方式,入栈顺序从右向左,被调用者平衡堆栈
.text:00401726 push 2 ; 参数3入栈
.text:00401728 push 1 ; 参数2入栈
.text:0040172A mov eax, [ebp+arg_0]
.text:0040172D push eax ; 参数1入栈
.text:0040172E call j_fun_c__ ; fun_c,stdcall调用方式,入栈顺序从右向左,被调用者平衡堆栈
.text:00401733 xor eax, eax
.text:00401735 pop edi
.text:00401736 pop esi
.text:00401737 pop ebx
.text:00401738 add esp, 0C0h ; 销毁局部变量,平衡堆栈
.text:0040173E cmp ebp, esp ; 比较esp的值是否正常
.text:00401740 call sub_4010B4 ; 调用检查esp的函数
.text:00401745 mov esp, ebp
.text:00401747 pop ebp
.text:00401748 retn
.text:00401748 main__ endp

fun_a()函数是cdecl调用,所以没有堆栈平衡,由调用者main()函数进行堆栈平衡

.text:00401480 fun_a__         proc near               ; CODE XREF: j_fun_a__j
.text:00401480
.text:00401480 var_C0 = byte ptr -0C0h
.text:00401480 arg_0 = dword ptr 8
.text:00401480 arg_4 = dword ptr 0Ch
.text:00401480 arg_8 = dword ptr 10h
.text:00401480
.text:00401480 push ebp ; EBP入栈保存
.text:00401481 mov ebp, esp ; 然后将堆栈指针ESP的值传递给EBP
.text:00401481 ; 如此一来在这个函数内只需要使用EBP就可对栈进行操作了。
.text:00401481 ; 这样做的好处是不需要对ESP做过多的操作
.text:00401483 sub esp, 0C0h ; 将ESP减0xC0
.text:00401489 push ebx
.text:0040148A push esi
.text:0040148B push edi ; 保存EBX、ESI、EDI
.text:0040148C lea edi, [ebp+var_C0]
.text:00401492 mov ecx, 30h
.text:00401497 mov eax, 0CCCCCCCCh
.text:0040149C rep stosd
.text:0040149E mov eax, [ebp+arg_0] ; 将参数1传递给eax
.text:004014A1 add eax, [ebp+arg_4] ; 将eax与参数2相加
.text:004014A4 add eax, [ebp+arg_8] ; 将eax与参数3相加
.text:004014A7 pop edi
.text:004014A8 pop esi
.text:004014A9 pop ebx
.text:004014AA mov esp, ebp
.text:004014AC pop ebp
.text:004014AD retn
.text:004014AD fun_a__ endp

fun_b()函数是fastcall调用,参数是由ECX与EDX这两个寄存器完成的,超出部分的参数依然采用压栈方式传递。被调用者fun_b()负责堆栈平衡。

.text:004014C0 fun_b__         proc near               ; CODE XREF: j_fun_b__j
.text:004014C0
.text:004014C0 var_D8 = byte ptr -0D8h
.text:004014C0 var_14 = dword ptr -14h
.text:004014C0 var_8 = dword ptr -8
.text:004014C0 arg_0 = dword ptr 8
.text:004014C0
.text:004014C0 push ebp
.text:004014C1 mov ebp, esp
.text:004014C3 sub esp, 0D8h
.text:004014C9 push ebx
.text:004014CA push esi
.text:004014CB push edi
.text:004014CC push ecx
.text:004014CD lea edi, [ebp+var_D8]
.text:004014D3 mov ecx, 36h
.text:004014D8 mov eax, 0CCCCCCCCh
.text:004014DD rep stosd
.text:004014DF pop ecx
.text:004014E0 mov [ebp+var_14], edx ; 将参数2的值传递给局部变量2
.text:004014E3 mov [ebp+var_8], ecx ; 将参数1的值传递给局部变量1
.text:004014E6 mov eax, [ebp+var_8] ; 将局部变量1的值传递给eax
.text:004014E9 add eax, [ebp+var_14] ; 将eax与局部变量2相加
.text:004014EC sub eax, [ebp+arg_0] ; 将eax与参数3相减
.text:004014EC ;
.text:004014EC ; 采用快速调用的函数参数是由exc与edx这两个寄存器完成的,
.text:004014EC ; 而超出部分的参数则依然要使用传统的压栈方式传递,以下就是本函数的参数与局部变量的结构
.text:004014EC ;
.text:004014EC ; 参数1:ecx
.text:004014EC ; 参数2:edx
.text:004014EC ; 参数3:ebp+0x8
.text:004014EC ; 局部变量1:ebp-0x8
.text:004014EC ; 局部变量2:ebp-0x1
.text:004014EF pop edi
.text:004014F0 pop esi
.text:004014F1 pop ebx
.text:004014F2 mov esp, ebp
.text:004014F4 pop ebp
.text:004014F5 retn 4
.text:004014F5 fun_b__ endp

fun_c函数是stdcall调用,函数返回时销毁局部变量,平衡堆栈。

.text:00401510 fun_c__         proc near               ; CODE XREF: j_fun_c__j
.text:00401510
.text:00401510 var_C0 = byte ptr -0C0h
.text:00401510 arg_0 = dword ptr 8
.text:00401510 arg_4 = dword ptr 0Ch
.text:00401510 arg_8 = dword ptr 10h
.text:00401510
.text:00401510 push ebp
.text:00401511 mov ebp, esp
.text:00401513 sub esp, 0C0h
.text:00401519 push ebx
.text:0040151A push esi
.text:0040151B push edi
.text:0040151C lea edi, [ebp+var_C0]
.text:00401522 mov ecx, 30h
.text:00401527 mov eax, 0CCCCCCCCh
.text:0040152C rep stosd
.text:0040152E mov eax, [ebp+arg_0] ; 将参数1传递给eax
.text:00401531 sub eax, [ebp+arg_4] ; 将eax与参数2相减
.text:00401534 sub eax, [ebp+arg_8] ; 将eax与参数3相减
.text:00401537 pop edi
.text:00401538 pop esi
.text:00401539 pop ebx
.text:0040153A mov esp, ebp
.text:0040153C pop ebp
.text:0040153D retn 0Ch ; 函数返回时销毁局部变量,平衡堆栈。
.text:0040153D fun_c__ endp

3、汇编改变特征小技巧

并不是所有的函数都只有用call指令才能调用,使用lea、push加jmp的组合也可以达到相同的目的。

例如将call Demo.013A1127可以转换为以下形式:

lea esi,return_addr ; 取到jmp Demo.013A1127指令后面的地址
push esi ; 将这个地址压入栈
jmp Demo.013A1127 ; 跳转到Demo.013A1127处执行函数代码

4、裸函数

C++使用naked标识创建的裸函数将不包含任何用户代码以外的指令,即便是函数末尾的retn也要用户自己来实现。

代码如下:

#include "stdafx.h"

__declspec(naked) int fun(int nNumA, int nNumB, int nNumC)
{
__asm
{
push ebp
mov ebp, esp
sub esp, 0x4
}
nNumA += (nNumB+nNumC); // 注意,此行为c语句。
__asm
{
mov eax, nNumA
add esp, 0x4
mov esp, ebp
pop ebp
retn
}
} int _tmain(int argc, _TCHAR* argv[])
{
printf("fun=%d", fun(argc,1,2));
return 0;
}

5、小结

采用不同的调用方式,反汇编代码不同。

  • a) 几乎全部函数调用方式都会用栈来传递参数,只有使用快速调用约定后且参数少于等于2时才会全部采用寄存器传参。

  • b) 函数起始部分:以push ebp和mov ebp,esp汇编指令开始

  • c) 每个函数由call指令调用,且以retn指令结尾。

  • d) 裸函数进行内联汇编可以改变以上的某些规律。

注:裸函数是指编译器生成汇编代码时不添加任何额外的指令,包括retn。

0x5 参考文章

《黑客免杀攻防》 软件逆向工程1-3

http://blog.csdn.net/dalerkd/article/details/41173623

【黑客免杀攻防】读书笔记7 - 软件逆向工程基础1(函数调用约定、Main函数查找)的更多相关文章

  1. 【黑客免杀攻防】读书笔记8 - 软件逆向工程基础2(if-else,三目运算符)

    0x1 if-else分支 if-else分支4种状态 1.1 以常量为判断条件的简单if-else分支 C源代码: 单层if-else判断,常量为判断条件 int _tmain(int argc, ...

  2. 【黑客免杀攻防】读书笔记5 - PE格式讲解

    0x01 MS-DOS头 MS-DOS头部的字段重点关注e_magic与最后一个e_lfanew是需要关注的. 第一个e_magic字段的值为4D5A,作用是可以作为判断这个文件是否是PE文件. 最后 ...

  3. 【黑客免杀攻防】读书笔记6 - PE文件知识在免杀中的应用

    0x1 PE文件与免杀思路 基于PE文件结构知识的免杀技术主要用于对抗启发式扫描. 通过修改PE文件中的一些关键点来达到欺骗反病毒软件的目的. 修改区段名 1.1 移动PE文件头位置免杀 工具:PeC ...

  4. 【黑客免杀攻防】读书笔记18-最终章Anti Rootkit

    1.免杀技巧的遏制 1.1.PE文件 入口点不在第一个区段或在最后一个区段 入口点处代码附近只有一小段代码 入口点在正常范围之外 入口点为一个无效的值,实际入口点为TLS的入口点 区段名重复或者不属于 ...

  5. 【黑客免杀攻防】读书笔记2 - 免杀与特征码、其他免杀技术、PE进阶介绍

    第3章 免杀与特征码 这一章主要讲了一些操作过程.介绍了MyCCL脚本木马免杀的操作,对于定位特征码在FreeBuf也曾发表过类似工具. VirTest5.0特征码定位器 http://www.fre ...

  6. 【黑客免杀攻防】读书笔记15 - 源码免杀、C++壳的编写

    1.源码免杀 1.1 定位产生特征的源码 定位文件特征 1.根据MyCCL的特征码定位工具,定位出有特征的地址 2.根据VS的反汇编窗口,输入有特征的地址得到特征地址与源码的关系 3.插入Messag ...

  7. 【黑客免杀攻防】读书笔记10 - switch-case分支

    0x1 switch-case分支 switch-case其实就是if-else语句的另一种体现形式.但大于3之后的switchc-case.编译器会对代码进行优化. 1.1 简单switch-cas ...

  8. 【黑客免杀攻防】读书笔记17 - Rootkit基础

    1.构建Rootkit基础环境 1.1.构建开发环境 VS2012+WDK8 1.2.构建基于VS2012的调试环境 将目标机.调试机配置在同一个工作组内 sVS2012配置->DRIVER-& ...

  9. 【黑客免杀攻防】读书笔记14 - 面向对象逆向-虚函数、MFC逆向

    虚函数存在是为了克服类型域解决方案的缺陷,以使程序员可以在基类里声明一些能够在各个派生类里重新定义的函数. 1 识别简单的虚函数 代码示例: #include "stdafx.h" ...

随机推荐

  1. BZOJ 4555: [Tjoi2016&Heoi2016]求和 (NTT + 第二类斯特林数)

    题意 给你一个数 \(n\) 求这样一个函数的值 : \[\displaystyle f(n)=\sum_{i=0}^{n}\sum_{j=0}^{i} \begin{Bmatrix} i \\ j ...

  2. BZOJ 400题纪念

    应该是最后一次纪念了吧! 想当年,我可是发过"BZOJ 10题纪念"的人--那时候(一年前?)的自己真的好菜啊,只能说掌握了c++的基础语法的样子.当时觉得省选级别的BZOJ题是世 ...

  3. 洛谷 P4097 [HEOI2013]Segment 解题报告

    P4097 [HEOI2013]Segment 题目描述 要求在平面直角坐标系下维护两个操作: 在平面上加入一条线段.记第 \(i\) 条被插入的线段的标号为 \(i\) 给定一个数 \(k\),询问 ...

  4. bzoj5017 炸弹 (线段树优化建图+tarjan+拓扑序dp)

    直接建图边数太多,用线段树优化一下 然后缩点,记下来每个点里有多少个炸弹 然后按拓扑序反向dp一下就行了 #include<bits/stdc++.h> #define pa pair&l ...

  5. activity之间的数据传递方法

    1  基于消息的通信机制 Intent--------boudle,extra 用这种简单的形式,一般而言传递一些简单的类型是比较容易的,如int.string等 详细介绍下Intent机制 Inte ...

  6. 在Android中afinal框架下實現sqlite數據庫版本升級的辦法

    public abstract void onUpgrade(SQLiteDatabase db,int oldVersion,int new Version) 這個方法在實現時需要重寫.   pub ...

  7. A1071. Speech Patterns

    People often have a preference among synonyms of the same word. For example, some may prefer "t ...

  8. Python基础学习(五)

    一.使用模块 已经了解了什么是模块,模块就是一个个文件的体,我们可以做不同的文件中引入各个模块文件,当然如果模块有冲突,还可以给模块文件的上层建立一个目录简称包,包名只能唯一,不能重名. 另外,一旦建 ...

  9. C语言实现KMP模式匹配算法

    next: /*! * Description: * author scictor <scictor@gmail.com> * date 2018/7/4 */ #include < ...

  10. C# 实现子窗体控制父窗体的方法

       我们来实现这样一个功能:当父窗体打开一个子窗体时隐藏父窗体的Panel,而当子窗体关闭时让Panel显示.实现的主要思路是创建一个子窗体的父类并在类中声明一个委托,当父窗体调用子窗体时绑定显示P ...