首页
社区
课程
招聘
[求助]高手请进!一道一道看似简单,其实很复杂的面试题目的思索!
发表于: 2012-10-5 20:54 10091

[求助]高手请进!一道一道看似简单,其实很复杂的面试题目的思索!

2012-10-5 20:54
10091
提问:这段程序是一个很古老的面试题目的测试,原始帖子在:http://bbs.pediy.com/showthread.php?t=104954
以下程序运行后,老是出错。可能是堆栈平衡问题。不过release版本还是不报错的,比较少了很多检查。不知道什么,请各位大侠指点。

#include<stdio.h>
extern "C" int __cdecl mainCRTStartup(void);
void print()
{
        #pragma comment(linker, "/entry:print")
        #pragma comment(linker, "/SECTION:.text,ERW")
        #pragma comment(lib, "msvcrt.lib")
        int mainCRTStartup();
        void main();
       
        __asm
        {
                MOV EAX, OFFSET main
                MOV BYTE PTR[EAX], 0xB8 //MOV EAX, 0x
                MOV DWORD PTR[EAX+1], OFFSET SHOWSTRING //将printf语句地址放在eax+1处
                MOV WORD PTR[EAX+5], 0xE0FF // JMP EAX:FFE0
        }
       
        mainCRTStartup();
        __asm
        {
                ret
        }

SHOWSTRING:
        printf("hello,world!\n");
}
void main()
{
}
我认为这个程序的执行流程是这样的:
首先执行print函数处(这个函数是关键):
执行到以下代码
__asm
        {
                MOV EAX, OFFSET main
                MOV BYTE PTR[EAX], 0xB8 //MOV EAX, 0x
                MOV DWORD PTR[EAX+1], OFFSET SHOWSTRING //将printf语句地址放在eax+1处
                MOV WORD PTR[EAX+5], 0xE0FF // JMP EAX:FFE0
        }
时,即将mov eax,OFFSET SHOWSTRING与jmp eax指令码写到main函数地址处。之后执行mainCRTStartup();这个crt启动函数显然会调用我们的名为main的函数。于是就执行了SHOWSTRING处的代码,之后mainCRTStartup();内部做一些清理性工作则会退出这个启动函数。然后,我们返回
__asm
        {
                ret
        }
这样就退出的print函数。我想,是不是此时返回时,栈顶作为返回地址的数据不和谐,让eip指向了一段不和谐的代码。于是乎不和谐的事情就出现了。要是这样,应该怎样做呢?

[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)

收藏
免费 0
支持
分享
最新回复 (16)
雪    币: 218
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
2
对C不太熟,但是你的代码里似乎没有显式的对堆栈进行操作,唯一觉得奇怪的地方就是mainCRTStartup写在print里面了
姑且猜一猜mainCRTStartup的汇编代码
push ebp
mov  ebp,esp
pop   ebp
ret
正常的执行的话这个函数在执行到ret的时候会做的事情是pop eip,因为call是压入返回地址到栈顶后跳转的,所以ret也会对堆栈进行操作,弹出返回地址到eip
但是你的函数没有call就顺序执行到了这个mainCRTStartup,这样栈顶的地址我不确定到底会是什么的,因为我不知道c启动函数到底干了什么...

最简单的还是丢调试器里看看就明白了
2012-10-5 21:10
0
雪    币: 468
活跃值: (52)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
3
#include<stdio.h>
extern "C" int __cdecl mainCRTStartup(void);
void print()
{
  #pragma comment(linker, "/entry:print")
        #pragma comment(linker, "/SECTION:.text,ERW")
  #pragma comment(lib, "msvcrt.lib")
  int mainCRTStartup();
  void main();
  
  __asm
  {
    MOV EAX, OFFSET main
    MOV BYTE PTR[EAX], 0xB8 //MOV EAX, 0x
    MOV DWORD PTR[EAX+1], OFFSET SHOWSTRING //将printf语句地址放在eax+1处
    MOV WORD PTR[EAX+5], 0xE0FF // JMP EAX:FFE0
  }
  
  mainCRTStartup();
  __asm
  {
    pop ebp
    ret
  }

SHOWSTRING:
  printf("hello,world!\n");
  __asm ret 16
}
void main()
{
__asm nop
__asm nop
__asm nop
__asm nop
__asm nop
}
这个程序应该如上面修改一下吧。
加了一个pop ebp,
和一个ret 16
加了5个nop是因为程序一开始修改main()函数的指令为jmp printf,但是要给main()函数至少5个字节的空间,否则修改main()函数就会破坏紧跟main()函数的代码。所以留出5个nop为指令空间占位。
pringf后面加个ret 16是因为printf实际是main跳转过来,带有main的4个参数,而且printf执行完后,编译器认为整个print函数完毕会自动生成pop ebp,因为入口处自动生成了push ebp,而此处不应该pop ebp ,所以执行ret 16代替编译器自动生成的pop ebp,ret.
mainCRTStartup();执行完以后加入 ret指令实际是没有用处的,因为mainCRTStartup()不会返回到这个ret指令处执行,而是执行了exitprocess系统退出函数了。假设mainCRTStartup();执行完后返回ret处,那么为了堆栈平衡,要先pop ebp,因为一开始就执行了pop ebp指令。
2012-10-5 21:55
0
雪    币: 468
活跃值: (52)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
4
引用:
pringf后面加个ret 16是因为printf实际是main跳转过来,带有main的4个参数,而且printf执行完后,编译器认为整个print函数完毕会自动生成pop ebp,因为入口处自动生成了push ebp,而此处不应该pop ebp ,所以执行ret 16代替编译器自动生成的pop ebp,ret.
改正:
因为是控制台程序,只有2个参数,所以应该改成ret 8,即
SHOWSTRING:
  printf("hello,world!\n");
  __asm ret 8
}
2012-10-5 22:20
0
雪    币: 468
活跃值: (52)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
5
再加一句吧,那个nop实际要至少7个nop,然而main()自动生成xor eax,eax和ret指令,占用了2,3个字节,因此5个nop是不会出错的。
2012-10-5 22:24
0
雪    币: 89
活跃值: (26)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
6
看雪还是看雪,逛看雪的人还真是不一般。佩服,分析的很好。没想到真的是堆栈平衡问题,感谢你的回答。我这里还有一点疑问:
1)“加了5个nop是因为程序一开始修改main()函数的指令为jmp printf,但是要给main()函数至少5个字节的空间,否则修改main()函数就会破坏紧跟main()函数的代码。所以留出5个nop为指令空间占位。”
经过调式,可以得到,这个main函数地址处的第一个指令其实是条jmp指令,其后是一大堆的nop指令而已。此面试题的题意是不要在main函数里面显示的加代码。
2)需要保证什么条件,才能使得启动函数正常返回。有时我调试的时候,根本就没有从 mainCRTStartup();返回。而是直接退出了。(这得好好分析这个函数)
3)我觉得要保持堆栈平衡,需要构造一些清栈的代码,并起到绕过编译器所加的检查代码。当然,要是能使得完全通过debug下的各个检查就更好了
再次感谢你的回答,给了提供了非常关键的思路。
2012-10-5 22:43
0
雪    币: 967
活跃值: (1138)
能力值: ( LV6,RANK:90 )
在线值:
发帖
回帖
粉丝
7
不知道aait 是否调试了程序 ,你的测试demo在vs2008过不了,
我调试了一下 我的编译工具是vs2008,
废话不多讲 直接上代码吧
#include<stdio.h>
extern "C" int __cdecl mainCRTStartup(void); 
void print()
{ 
#pragma comment(linker, "/entry:print")
#pragma comment(linker, "/SECTION:.text,ERW")
#pragma comment(lib, "msvcrt.lib")
	int mainCRTStartup();
	void main();

	__asm
	{
		MOV EAX, OFFSET main
		MOV BYTE PTR[EAX], 0xE9 //MOV EAX, 0x 
				MOV ecx,OFFSET SHOWSTRING;
			sub ecx, offset main;
			sub ecx,5
			MOV DWORD PTR[EAX+1], ecx //将printf语句地址放在eax+1处
			
			
	}

	mainCRTStartup();
	__asm
	{
	

		ret
	}

SHOWSTRING:
	printf("hello,world!\n");
	__asm
	{
		ret
	}
}
void main()
{
	
}
2012-10-5 23:47
0
雪    币: 468
活跃值: (52)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
8
回复7楼,你不修改楼主的源代码肯定是要出错了。主要是因为程序修改main()函数,而main()函数只有2到3,4个字节,所以修改main()函数会冲掉紧跟着main()函数的某个子程序的某些字节,肯定就会崩溃了。我调试了,好象紧跟着main()函数的是缓冲区安全检查函数secutity_cookie,破坏了这个函数入口处的几个字节肯定是要崩溃的。所以应该在main()函数里面加5个到7个nop.给修改main()函数留出指令空间,应该是7个字节的指令空间。
另外printf执行完后,楼主源代码没有ret返回,而是编译器自动生成pop ebp,ret指令,这个pop ebp是不队的,
堆栈不平衡了,所以手工添加ret 8指令,代替编译器自动生成的pop ebp,ret.
以上修改我是经过调试器调试了的。请使用我修改的代码编译。
因为printf一闪而过,所以最好在printf后面加一个getcha();
2012-10-6 06:41
0
雪    币: 468
活跃值: (52)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
9
#include<stdio.h>
extern "C" int __cdecl mainCRTStartup(void);
void print()
{
  #pragma comment(linker, "/entry:print")
        #pragma comment(linker, "/SECTION:.text,ERW")
  #pragma comment(lib, "msvcrt.lib")
  int mainCRTStartup();
  void main();
  
  __asm
  {
    MOV EAX, OFFSET main
    MOV BYTE PTR[EAX], 0xB8 //MOV EAX, 0x
    MOV DWORD PTR[EAX+1], OFFSET SHOWSTRING //将printf语句地址放在eax+1处
    MOV WORD PTR[EAX+5], 0xE0FF // JMP EAX:FFE0
  }
  
  mainCRTStartup();
  __asm
  {
    pop ebp
    ret
  }

SHOWSTRING:
  printf("hello,world!\n");
  __asm ret 8
}
void main()
{
__asm nop
__asm nop
__asm nop
__asm nop
__asm nop
__asm nop
__asm nop
}
2012-10-6 07:17
0
雪    币: 89
活跃值: (26)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
10
这个答案在vs2008下还是通不过。不过你们的回答给了我很好的思路。非常谢谢你们的回答。如果可以,希望能再改改。这个堆栈平衡很有问题。
1)尤其是怎样构造是的main函数正常返回,使得crt启动函数正常调用main函数,并正确清理资源返回。
2)要保持堆栈平衡,需要构造一些清栈的代码,并起到绕过编译器所加的检查代码。当然,要是能使得完全通过debug下的各个检查就更好了
3)main函数起始处只是一系列int 3指令罢了,不用nop吧!并在main里面nop貌似没啥作用!因为我们的__asm
  {
    MOV EAX, OFFSET main
    MOV BYTE PTR[EAX], 0xB8 //MOV EAX, 0x
    MOV DWORD PTR[EAX+1], OFFSET SHOWSTRING //将printf语句地址放在eax+1处
    MOV WORD PTR[EAX+5], 0xE0FF // JMP EAX:FFE0
  }根本就不是将mov eax,OFFSET SHOWSTRING与jmp eax指令码写到我们所见到的main函数里面。而只是写在了jmp指令处。
2012-10-6 23:40
0
雪    币: 967
活跃值: (1138)
能力值: ( LV6,RANK:90 )
在线值:
发帖
回帖
粉丝
11
非要我说脏话,你为什么不测试一下我的代码啊
你难道眼睛有问题吗?
真的看不出,我给出的代码和你原来代码的变化吗?

汗 以后打死不回这样的帖子了,
2012-10-7 17:53
0
雪    币: 89
活跃值: (26)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
12
主要是你更新的解答。所以我就没有看。是在抱歉。
2012-10-7 18:55
0
雪    币: 89
活跃值: (26)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
13
问最后一个问题:两种跳转方式                        
MOV EAX, OFFSET main
                        MOV BYTE PTR[EAX], 0xE9 //构造jmp 相对地址 指令并放在main函数处
                        MOV ecx,OFFSET SHOWSTRING;
                    sub ecx, offset main;
                    sub ecx,5
                        MOV DWORD PTR[EAX+1], ecx //将printf语句地址放在eax+1处
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
                        //MOV EAX, OFFSET main
                        //MOV BYTE PTR[EAX], 0xB8 //MOV EAX, 0x
                        //MOV DWORD PTR[EAX+1], OFFSET SHOWSTRING    //将printf语句地址放在eax+1处
                        //MOV WORD PTR[EAX+5], 0xE0FF // JMP EAX:FFE0
怎么下面构造的跳转会出问题呢?能不能分析下??elianmeng,aait
2012-10-7 22:12
0
雪    币: 468
活跃值: (52)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
14
[QUOTE=xwl刘晓伟;1107119]问最后一个问题:两种跳转方式                        
MOV EAX, OFFSET main
                        MOV BYTE PTR[EAX], 0xE9 //构造jmp 相对地址 指令并放在main函数处
                        MOV ecx,OFFSET SHOWSTRING;
                    s...[/QUOTE]

你并没有看懂我以前的回答, 7个nop的作用,或者用7个int 3也可以。第一种跳转占用5个字节,第二种跳转占用7个字节,如果直接main()是空的话,main()编译以后只有2个或者3个字节,没有冲掉紧跟main()的代码是侥幸,如果稳妥一点,还是写入7个空代码吧。
2012-10-8 11:13
0
雪    币: 89
活跃值: (26)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
15

跟进去,可以看到,只是一系列jmp指令而已。并没有真正到main函数处

妈的,我不会贴图片,图片挂了。网页太不方便了。
2012-10-8 23:10
0
雪    币: 209
活跃值: (143)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
16
原题目中貌似没extern "C"一句

原题:
#include "stdio.h"
void print()
{
*
}

void main()
{
}

在*号处加一段代码,显示出"hello,world".


换一种思路的解法:
#include "stdio.h"
void print()
{
}
void main()
{
    printf("Hello world.\n");
#define main test
}

void main()
{
}


加粗部分为新加入代码
2012-10-9 08:57
0
雪    币: 89
活跃值: (26)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
17
十分感谢你的回答!!用宏是能很好的解决那个面试题,也可以用全局变量或静态变量在main之前初始化的特性来解答此面试题。甚至可以用writeFile来代替printf来输出到控制台。(其实printf应该也是调用的此函数,并且此函数不是c运行库的函数,而是windows库函数)。但是我这里提问的是,我贴出来的做法的问题出在哪里?
2012-10-9 13:09
0
游客
登录 | 注册 方可回帖
返回
//