4.27日更新
😭我搞了一天博客,于是乎上周日那天鸽了,今天刚搞好,一看已经13.24了,不多说了学习!
今日任务:小黄书 + 对抗反汇编 x86
启动函数
mainCRTStartup函数代码片段分析
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63
| //预编译宏 #else /* _WINMAIN_ */ #ifdef WPRFLAG //宽字符版控制台启动函数 void wmainCTRStartup( #else /* WPRFLAG* / //多字节版控制台启动函数 void mainCRTStartup( #endif /* WPRFLAG */ #endif /* _WINMAIN_ */ void ) { //获取版本信息 _osver = GetVersion(); _winminor = (_osver >> 8) & 0x00FF; _winmajor = _osver & 0x00FF; _winver = (_winmajor << 8) + _winminor; _osver = (_osver >>16) & 0x00FFFF; //堆空间初始化过程,在这个函数中,制定了程序中堆空间的起始位置 //_MT是多线程标记 #ifdef _MT if(!_heap_init(1)) #else /* _MT */ if(!_heap_init(0)) #endif /*_MT */ fast_error_exit(_RT_HEAPINIT); //初始化多线程环境 #ifdef _MT if(!_mtinit()) fast_error_exit(_RT_THREAD); #endif /* _MT* / _try{ //宽字节处理代码略 //多字节版获取命令行 _acmdln = (char*)GetCommandLineA(); //多字节版获取环境变量信息 _aenvptr = (char*)_crtGetEnvironmentStringsA(); //多字节版获取命令行信息 _setargv(); //多字节版获取环境变量信息 _setenvp(); #endif /* WPRFLAG */ //初始化全局数据和浮点寄存器 _cinit(); //窗口程序处理代码略 //宽字符处理代码略 //获取环境变量信息 _initenv = _environ; //调用main函数,传递命令行参数信息 mainret = main(_argc,_argv,_environ); #endif /* WPRFLAG */
#endif /* _WINMAIN_ */ //检查main函数返回值执行析构函数或atexit注册的函数指针,并结束程序 exit(mainret); } //退出结束代码略 }
|
这里面👴通读了一下,有很多的函数不知道干什么的,总结一下:
GetVersion
函数:获取当前运行平台的版本号,控制台程序运行在Windows模拟的DOS下,因此这里获取的版本号为MS-DOS的版本信息
_heap_init
函数:用于初始化堆空间,在函数实现中使用HeapCreate申请堆空间,申请空间的大小由_heap_init
传递的参数决定,_sbh_heap_init
函数用于初始化堆结构信息
GetCommandLineA
函数:获取命令行参数信息的首地址
_crtGetEnvironmentStringsA
函数:获取环境变量信息的首地址
_setargv
函数:根据GetCommandLineA
获取命令行参数信息的首地址并进行参数分析,将分离出的参数的个数保存在全局变量_argc
中,将分析出的每个命令行参数的首地址存放在数组中,并将这个字符指针数组的首地址保存在全局变量_argv
中,从而得到命令行参数的个数以及命令行参数的信息
_setenvp
函数:根据_crtGetEnvironmentStringA
函数获取环境变量信息的首地址进行分析,将得到的每条环境变量字符串的首地址存放在字符指针数组内,并将这个数组的首地址存放在全局变量env
中
所以当我们的main函数得到这三个参数,_argc
,_argv
,env
三个全局变量,他们作为参数以栈传递的方式传到main函数中
所以,这个初始化函数是无参数也是无返回值的,c++规定全局对象和静态对象必须在main函数前构造,在main函数返回后析构,所以这里是用来代理构造析构函数的
_cinit函数代码
1 2 3 4 5 6 7
| //用于初始化寄存器 if(_FPinit != NULL) (*_FPinit)(); //初始化浮点寄存器 //用于初始化C语法中的数据 _initterm( _xi_a, _xi_z); //用于初始化C++语法中的数据 _initterm( _xc_a , _xc_z);
|
这里_Fpinit
是一个全局函数指针,类型是_PVFV
,如果编译器扫描代码的时候,发现有浮点计算,则这个指针保存了初始化浮点寄存器的代码地址,否则就是0,如果浮点寄存器没有被出刷,会有异常错误👴在上周的时候说过了,参数xi_a
为函数指针数组的其实地址,_xi_z
为结束地址,第一个_initterm初始化的都是C支持库中所需要的数据
_initterm函数代码
1 2 3 4 5 6 7 8 9 10 11
| static void _cdecl _initterm ( _PVFV * pfbegin, _PVFV * pfend ) { //遍历数组的各元素 while(pfbegin < pfend){ //若函数指针不为空,则执行该函数 if(*pfbegin!=NULL) (**pfbegin)(); ++pfbegin; } }
|
C++初始化操作会在第二次的_initterm
进行调用,一般是全局变量或者静态对象的初始化函数(这里😥不咋懂,书上说第10章会说)
正常的mainCRTStartup函数会根据编译器版本的不同所有不同!👴早就猜到了,如vs2005,其中的mainCRTStartup
变成_tmainCRTStartup
,在我们的默认情况下,入口函数是main,这时候会从mainCRTStartup启动,再传入main所需要的三个参数argc
,argv
,env
,最后再调用main
如果我们重新指定入口函数
可以看到没有调用mainCRTStartup函数,我们可以再测试一下
我再写一个新的函数调用并不是main来看一下
如果我没有调用mainCRTStartup函数,我们的堆空间是没有被初始化的,所以用到堆就会报错
我们看一下,正常的情况
张长是可以分配的,所以说如果书用了malloc函数,没有初始化堆空间会出错的
main函数识别
VC++6.0main函数特征,三个参数:命令行参数个数,命令行参数信息,环境变量信息,main是启动函数中唯一一个具有3个参数的函数,WinMain也是启动函数中为一具有4个参数的函数
VC++6.0,main函数被调用前,要先调用的函数:
GetVersion()
_heap_int()
GetCommandLineA()
_crtGetEnvironmentStringsA()
_setargv()
_setenvp()
_cinit()
这些函数调用结束后就会调用main函数,根据main函数调用的特征,将argc,argv,env压入栈作为参数
用ida可以看到_mainCRTStartup的大概流程
我们用od去看看做了什么,od会直接暂停在程序的入口处,并不是定位在main函数的位置,我们试着通过main函数的特性查找一下他在哪个位置
自己根据分析,我在断点的位置就是main的函数入口
我用书上的代码做一个例子,便于分析!🏃♂️奥里给冲!👍
OD反汇编信息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| //省略部分代码 //OD识别函数名称为GetCommandLineA 00401210 |. FF15 38514200 call dword ptr ds:[<&KERNEL32.GetCommand>] //得到命令行参数 00401216 |. A3 444F4200 mov dword ptr ds :[424F44],eax //根据main函数的特性,此处为_crtGetEnvirinmentStringsA()调用 0040121B |. E8 E0240000 call ProgramE.00403700 00401220 |. A3 BC354200 mov dword ptr ds:[4235BC],eax //根据main函数特性,此处为函数_setargv()调用 00401225 |. E8 C61F0000 call ProgramE.004031F0 //根据main函数特性,此处为函数_setenvp()调用 0040122A |. E8 711E0000 call ProgramE.004030A0 //根据main函数特性,此处为函数_cinit()调用 004012FF |. E8 8C1A0000 call PromgramE.00402CC0 00401234 |. 8B0D 00364200 mov ecx,dword ptr ds:[423600] 0040123A |. 890D 04364200 mov dword ptr ds:[423604],ecx 00401240 |. 8B15 00364200 mov edx,dword ptr ds:[423600] //压栈传参,环境变量信息 00401246 |. 52 push edx 00401247 |. A1 F8354200 mov eax,dword ptr ds:[4235F8] //压栈传参,命令行参数信息 0040124C |. 50 push eax 0040124D |. 8B0D F4354200 mov ecx,dword ptr ds:[4235F4] //压栈传参,命令行参数个数 00401253 |. 51 push ecx //此处为卖弄函数的调用出,跟进到函数中便是main函数的实现代码流程 00401254 |. E8 ACFDFFFF call ProgramE.00401005
|
大概👴总结一下,一般我们用od的时候,来到的应该是mainCRTStartup的位置,那么我们寻找main函数的时候,我们需要去找到相应的调用前的函数的流程结束,从GetVersion()
获取平台版本号 _heap_init
初始化堆空间 GetCommandLineA
获取命令行参数信息的首地址,_crtGetEnvironmentStringA
获取环境变量信息的首地址,_setargv
得到命令行参数个数,以及命令行参数信息,_setenvp
获取环境变量字符串的首地址 _cinit
全局数据和浮点寄存器初始化,之后main函数所需要的参数压入堆栈,调用main
👍小黄书的第三章也算完结了,明天开始搞第四章,😡家里实在是太冷了,我倒在床上就睡着了,呜呜呜,准备开始深入搞一下对抗反汇编x86的问题,在52破解上也找到了相关的文章,准备学习一下!
对抗反汇编x86
52破解原帖子:https://bbs.pediy.com/thread-259064.htm
Gstalker师傅给了一个Opcode在线转化网站:https://onlinedisassembler.com/odaweb/
相同的跳转指令
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| #include<stdio.h> void 相同的跳转指令() { printf("定位"); _asm { jz _P2 jnz _P2 _P1 : __emit 0xE8 } _P2: return; } int main(int argc, char* argv[]) { 相同的跳转指令(); return 0; }
|
在ida中会骗到ida,实际上动态分析调试的时候,会转到相应的pop的功能,但是静态分析会欺骗到
我跳转过去看一下
直接退出堆栈,就不会像ida中那样说的是对应的CALL
固定条件的跳转指令
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| #include<stdio.h> void 固定条件的跳转指令() { printf("定位"); _asm { xor eax, eax jz _P2 __emit 0xE9 } _P2: return; } int main(int argc, char* argv[]) { 固定条件的跳转指令(); return 0; }
|
可以看到我们直接是跳转到jmp这里
但是我们正常进行动态分析的时候不会这样,还是进行一个pop出原来现场的问题
无效的反汇编码:内部跳转的jmp指令
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| #include<stdio.h> void 无效的反汇编码1() { //内部跳转的jmp指令 printf("定位"); _asm { __emit 0xEB __emit 0xFF __emit 0xC0 __emit 0x48 } return; } int main(int argc , char* argv[]) { 无效的反汇编码1(); return 0; }
|
动态调试直接过来就可以了
无效的反汇编码:多层内部调转序列
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| #include<stdio.h> void 无效的反汇编码2() { //多层内部调转序列 printf("定位"); _asm { __emit 0x66 __emit 0xB8 jmp _P1 __emit 0x31 __emit 0xC0 __emit 0x74 __emit 0xFA __emit 0xE8 } _P1: printf("真实代码!"); return; } int main(int argc , char* argv[]) { 无效的反汇编码2(); return 0; }
|
第一次的跳转:
第二次的跳转:
第三次的跳转:
去ida看一下:
滥用返回指针:
在反汇编部分我们看不出函数有调用_P2就retn了,成功欺骗了F5
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| #include<stdio.h> void 滥用返回指针() { printf("定位"); _asm { call _P1 _P1 : add[esp], 5 retn } _P2: printf("模糊函数边界!"); //这里会被执行 return; } int main(int argc , char* argv[]) { 滥用返回指针(); return 0; }
|
感觉正常的程序调试没什么毛病,我们去ida看一下
直接就是栈指针不平衡的问题了,不能f5需要手动去修复
滥用结构化异常处理:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| void 滥用结构化异常处理() { printf("定位"); _asm { mov eax, _PE1 push eax push fs : [0] mov fs : [0], esp xor ecx, ecx div ecx retn _PE1: mov esp,[esp+8] mov eax, fs : [0] mov eax, [eax] mov eax, [eax] mov fs : [0], eax add esp, 8 } printf("结构化异常"); return; }
|
挫败的栈帧分析:
F5后发现有很多参数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| #include<stdio.h> void 挫败的栈帧分析() { printf("定位"); _asm { xor eax, eax add eax, 2 ret 0xff } printf("挫败的栈帧分析"); return; } int main(int argc , char* argv[]) { 挫败的栈帧分析(); return 0; }
|