滴水三期:day09.2-程序的真正入口

一、程序入口

1.代码执行入口main

  • 我们知道C语言的代码入口是main函数,即代码是从main函数开始执行的。但是这不是程序的真正的入口

2.入口函数

1)mainCRTStartup()
  • 如何查看程序的入口:使用VC6自带的堆栈调用窗口

    • F7生成可执行文件—F9设置断点—再F5运行到断点处停下

    • 点击View—Debug Windows–Call Stack,打开堆栈调用窗口

      image-20211202195012960 image-20211202195146838

    • 可以看到有一个叫KERNEL32的东西(先不管是什么,后面会学),它调用了mainCRTStartup()函数,而这个函数又调用了我们写的代码程序的入口main

  • 所以一个程序的真正的入口函数其实是mainCRTStartup(),这个入口不要轻易的修改,因为这个函数做了很多初始化工作,如果修改了这个入口后面的程序会出现很多问题

  • 那么如何修改程序入口呢?

    • Project—Settings—Link—选择Category为Output—在Entry-point symbol中填写想要修改的入口函数

      image-20211202200124664 屏幕截图 2021-12-02 200505
  • 那么**mainCRTStartup()**函数做了哪些初始化工作:

    //调用了下面特定功能的函数
    GetVersion() 	   //获得当前操作系统的版本
    _heap_init() 	   //初始化堆的空间大小
    GetCommandLineA()  //获取命令行的参数 	
    _crtGetEnvironmentStringsA()   //获取环境变量
    _setargv()	   //等等
    _setenvp()	
    _cinit()	
    

    上述初始化的东西都是控制台要用到的东西

2)其他入口函数
  • mainCRTStartupwmainCRTStartup控制台环境下多字节编码和Unicode 编码的启动函数
  • WinMainCRTStartupwWinMainCRTStartupwindows 环境下多字节编码和Unicode 编码的启动函数

3.怎么找程序的入口(用于控制台程序)

  • 我们使用堆栈调用窗口双击mainCRTStartup()函数,进去看一下它调用的main函数是怎么定义的

    image-20211202201934013
  • 可以发现main函数有三个参数,所以如果我们想找程序入口,先找到GetVersion() 、_heap_init() 、
    GetCommandLineA() 、_crtGetEnvironmentStringsA() 、_setargv()、_setenvp() 、_cinit()中的任意一个函数,那么再在它们的后面找,找有三个参数的方法,如果找到了,就是main入口

  • 现在用OD打开一个程序,OD会自动识别二进制,形成注释,所以可以在反汇编窗口中右侧看到注释,会显示那一行指令表示哪一个函数,只要看到了上述函数,再往后找,找有传入了三个参数的方法,虽然用push和add esp,x的方法判断参数个数不一定正确,但是多半都满足这个规则,所以如果找到了一个call指令前有三个入栈,且后面跟了一个add esp,0xC 来平衡堆栈那么多半调用的就是main函数,按回车跟踪地址就可以看到main函数中的内容

    image-20230110170941297
  • 先进过一个jmp过度中转后,就可以看到这个函数就是我们写的main方法,当中就是我们在main中写的代码翻译过来的汇编指令

    image-20211202203903210
  • 找到以后就可以开始分析当中调用的函数了:分析参数,跟踪进去分析功能等,如果看到几个call后面跟的相同的地址,那么说明一个函数被调用了好几次;还注意最后一个call上面的指令是add esp,48/cmp ebp,esp这个就是编译器自动调用的检查堆栈平衡的函数,就不用逆向分析了

二、作业

1.将CallingConvention.exe逆向成C语言

1)找到main函数入口
  • 先找到程序的main入口:我们知道在mainCRTStartup()程序入口函数做完初始化工作后,才会调用main函数,所以我们要找GetVersion(),_heap_init(),GetCommandLineA()等由mainCRTStartup()调用的函数,找到它们后,再往后找找,如果遇到一个call指令前压入了三个参数,且在这个call指令后平衡堆栈的add esp指令中的后面操作数为0xc,那么就表名这个call指令很可能就是调用了main函数

    image-20211204083245545
  • 找到后回车call指令跟踪地址进去,先经过jmp跳转,再就可以看到main函数了,就可以开始对main函数分析还原它的代码了

    image-20211204083428888
  • 由于main函数也是一个函数,所以开始也要保留栈底、提升堆栈、保留现场、填充堆栈

    image-20211204083640909
  • 接着就可以看到main函数调用了第一个函数A,现在可以初步猜测这个函数一共有五个参数,由于参数是倒着从后往前存储的,所以第一个函数A的参数依次为1,3,4,6,7;且前两个使用寄存器存储,可以推断出使用的是**__fastcall调用约定**,接着让我们跟踪call指令进入第一个函数

    image-20211204083725622


2)分析调用的第一个函数A
  • 在0x401128处设置断点,让程序执行到这里停下,接着F8执行到call指令,再按F7进入call,经过jmp中转,最终可以进入到第一个函数A中

    image-20211204084337199
  • 前面几行还是做一些准备工作,只是多了一步保留ecx的工作,因为ecx中存储了参数的值,但是填充缓冲区时要使用到它,所以要先将它的值保存起来,ecx使用完后再将值重新恢复回去

    image-20211204084532549
  • 再下面两行是使用__fastcall调用约定时,编译器会自动将用两个寄存器存储的参数值放入内存中,方便后面对参数的运算操作,所以这一行不能理解为函数A又定义了两个局部变量!!!

    image-20211204085218840
3)分析A函数中的子函数Ason1
  • 再往下分析这个函数内部又调用了一个函数Ason1,且用到了三个参数,参数依次为A函数前三个参数1,3,4(因为参数是倒着存);可以看到call指令下面没有平衡堆栈的操作,且存储参数的是内存,所以推测Ason1函数使用的是__stdcall调用约定;将参数依次入栈,接着进入call指令,经过jmp中转,进入到Ason1函数中

    image-20211204085448473
  • 同样函数先做准备工作:保留栈底,提升堆栈,保留现场,填充缓冲区

    image-20211204090240546
  • 接着下面三行就是Ason1函数的功能1+3+4,将结果8存入eax中

    image-20211204090401880
  • 接着就是函数的收尾工作:恢复现场,降低堆栈,并且返回。唯一注意的是这里使用retn 0xC来内平栈,所以结合起来判断,Ason1函数确实有3个参数

    image-20211204090704095
  • 此时第一个函数已经调用完成,将它的返回值8存入缓冲区中,说明这个返回值在函数A中有一个新的局部变量c接着,即A函数定义了新的局部变量c并且赋值为8

4)分析A函数中第二个子函数Ason2
  • 结合push和add指令可以推断出Ason2函数使用了2个参数,第一个参数x赋值为A函数参数i1第二个参数y赋值为A函数参数i2
image-20211204091858043
  • 接着进入Ason2函数中,经过jmp中转后,可以看到Ason2函数

    image-20211204092114635
  • 同样的先做准备工作(这里不再赘述)

    image-20211204092203966
  • 接着下面的两行就是Ason2函数的功能:1+3,将结果4存入eax中

    image-20211204092237949
  • 接着就是函数的收尾工作,最后返回到main函数中,执行add esp,8指令,外平栈

    image-20211204092455825image-20211204092549650

  • 并且将Ason2返回结果4存入A函数的缓冲区,则说明A函数定义了新的局部变量d来接Ason2的返回值4,即A中新的局部变量d赋值为4

    image-20211204092706318
5)分析Ason2第二次调用
  • 可以看到下面又调用了一次Ason2函数,因为call指令后面的地址就是Ason2函数的起始地址和上一个call后面地址一样

  • 第二次调用Ason2,结合push和add esp指令可以得知Ason2有两个参数,第一个参数为接收第一次Ason2的返回值4的变量c第二个参数为接收Ason1的返回值8的变量d,接着进入到call指令中,经过jmp指令,可以看到Ason2函数

    image-20211204092921111
  • 上面已经详细分析过Ason2函数了,这里我们只提它的功能:4+8,将结果12存入eax中,作为Ason2的返回值

    image-20211204093847822
  • 接着就返回到A函数中,执行add esp指令来外平栈

    image-20211204094145533
6)A函数结束
  • 可以看到后面的指令就是先是恢复现场

    image-20211204094256434
  • 此函数就是编译器自动调用的检查堆栈是否平衡的函数,所以如果为了还原C代码不用跟踪

  • 接着就是降低堆栈,并且函数返回,此处有一个retn 0xC表示内平栈,此时A函数就调用结束了,且第二次调用的Ason2返回值作为A函数的返回值

    image-20211204094456866

7)分析B函数(不用逆向分析)
  • 当A函数结束回到main函数时,将eax中的值存入了main函数的缓冲区,所以此时main中应该有一个局部变量m,来接收A函数返回值,即m赋值为12

    image-20211204105642449

  • 接着看到下面又调用了一个函数B,函数B有两个参数,第一个参数t1为main中局部变量m,赋值为12;第二个参数为一个数字—0x0041F10C,注释后面写的是"%d"的ASCII码?(可能是一个地址编号,也可以是一个数而已,我们进入到B函数中看看此参数是如何被使用的,就知道了)

    image-20211204110541321
  • 进入到B函数中

    image-20211204110702611
  • 可以看到开始是保留栈底,然后提升堆栈,接着保留现场,再将B函数的第一个参数t1所存入地址编号赋给eax,则eax的值为0x0019FEDC,即[ebp+C]

    image-20211204110823602 image-20211204111028573

  • 可以看到此时将eax中的值存入[ebp-C]内存中,即将t1参数的地址编号存入内存中;

  • 接着比较参数2(0x0041F10C)和0的大小,此时ZF标志位为0;

  • 然后有一个jnz指令,如果z=0则跳转到0x0040B813这个地址

    image-20211204111410083
  • 先停下!!!这个函数和我们学的不太一样,一般人怎么会把一个像地址编号的十六进制数放进去,不太像我们写的代码,所以不妨先进去看看里面是什么,可以看到注释一栏有很多信息,大概和报错信息很相像,如果我们把一些标志位改了营造一种错误的假象,让它进入报错信息的函数看看会有什么效果,我们发现最后会弹出一个框,所以和我们预想的一样,这也是一个处理错误的函数,具体是什么不清楚,但是目前来说不用我们分析。故直接跳过

    image-20211204115614187
8)main函数结束
  • 我们跳过上面的类似于报错的函数(可能是编译器自己调用的),但是发现下面有一条**xor eax,eax的指令**,这条指令会把eax寄存器中的值清零,所以main函数应该就是返回一个0值,那么我们分析出main函数应该有返回类型,为int型,最后有一句return 0;。然后继续向下分析,先恢复现场;然后编译器又自动调用了一个检查堆栈是否平衡的函数,所以不需要我们分析;接着就是降低堆栈;最后函数返回,至此main函数的调用结束,分析结束

9)根据分析还原代码
#include "stdafx.h"

//Ason1函数
int __stdcall Ason1(int j1,int j2,int j3){  //使用__stdcall调用约定
    return j1 + j2 + j3;
}

//Ason2函数
int Ason2(int x,int y){   //使用c语言默认的__cdecl调用约定
    return x + y;
}

//A函数
int __fastcall A(int i1,int i2,int i3,int i4,int i5){   //使用__fastcall调用约定 
    int c;
    int d;
    c = Ason1(i1,i2,i3); //8
    d = Ason2(i1,i2); //4
    return Ason2(c,d); //12 
}

//代码执行入口
int main(int argc, char* argv[]) 
{
    int m;
    m = A(1,3,4,6,7);  //12
	return 0;
}

注意逆向的时候如果一个函数中嵌套了另一个函数,那么这个嵌套的函数必须写在这个函数的上面,不然编译器无法识别这个嵌套函数是否定义过

2.疑问

  • 为什么此程序中会出现这样的一段指令,是干什么用的?

    image-20211204121744483

3.总结一下

  • 函数的局部变量是按照顺序从[ebp-4]开始往上存,即存储在缓冲区中;而函数的参数是在函数调用之前编译器就帮我们做了,而且是倒着存储入栈或者内存,所以取的时候取参数是从[ebp+8]开始往下取,堆栈中从上到下就是参数在函数中从左到右的顺序

    image-20211204151952380 image-20211204151839774

  • 5
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值