用OD加载程序,加载Map文件,运行程序,点击"Crash",弹出一个错误对话框,
点确定,程序终止,此时看Alt+K,查看调用堆栈:
调用堆栈: 主线程
地址 堆栈 函数过程 / 参数 调用来自 结构
0012F1A4 6F029C6F 包含ntdll.KiFastSystemCallRet SYSFER.6F029C6D 0012F2A0
0012F1A8 7C81CD96 包含SYSFER.6F029C6F kernel32.7C81CD94 0012F2A0
0012F2A4 7C81CDEE ? kernel32.7C81CD34 kernel32.7C81CDE9 0012F2A0
0012F2B8 0040F55A ? kernel32.ExitProcess TestFloa.___crtExitProcess+0E 0012F2B4
0012F2BC 000000FF ExitCode = FF
0012F2C0 0040F774 ? <TestFloa.___crtExitProcess> TestFloa.0040F76F
0012F2FC 0040F7AA ? TestFloa.0040F6AA TestFloa.__exit+8
0012F30C 0040F51C 可能 <TestFloa.__exit> TestFloa.__amsg_exit+1E 0012F3D0
0012F310 000000FF status = FF (255.)
0012F31C 0041AE26 <TestFloa.__amsg_exit> TestFloa.0041AE21 0012F3D0
0012F324 00413E10 包含TestFloa.0041AE26 TestFloa.__woutput_l+59D 0012F3D0
0012F7C8 0040EE90 ? <TestFloa.__woutput_l> TestFloa.__snwprintf+89 0012F3D0
0012F80C 00420D86 <TestFloa.__snwprintf> TestFloa.?OnBnClickedBtnCrash@ 0012F808
0012F8BC 00402802 <TestFloa.?OnBnClickedBtnCrash@CTest TestFloa.?_AfxDispatchCmdMsg@@ 0012F8B8
0012F8CC 00402A0F <TestFloa.?_AfxDispatchCmdMsg@@YGHPA TestFloa.?OnCmdMsg@CCmdTarget@ 0012F8C8
0012F8FC 0040300C <TestFloa.?OnCmdMsg@CCmdTarget@@UAEH TestFloa.?OnCmdMsg@CDialog@@UA 0012F8F8
0012F920 00407F23 <TestFloa.?OnCmdMsg@CDialog@@UAEHIHP TestFloa.?OnCommand@CWnd@@MAEH 0012F91C
0012F970 00408937 可能 <TestFloa.?OnCommand@CWnd@@MAEH TestFloa.?OnWndMsg@CWnd@@MAEHI 0012F96C
由调用栈可知程序是在TestFloa.__woutput_l+59D处挂掉的
重新打开程序,在_woutput_l程序中进行跟踪:
00413DDB |> \8B03 |mov eax, dword ptr [ebx]
00413DDD |. 83C3 08 |add ebx, 8
00413DE0 |. 8945 88 |mov dword ptr [ebp-78], eax
00413DE3 |. 8B43 FC |mov eax, dword ptr [ebx-4]
00413DE6 |. 8945 8C |mov dword ptr [ebp-74], eax
00413DE9 |. 8D45 9C |lea eax, dword ptr [ebp-64]
00413DEC |. 50 |push eax
00413DED |. FF75 94 |push dword ptr [ebp-6C]
00413DF0 |. 0FBEC2 |movsx eax, dl
00413DF3 |. FF75 E8 |push dword ptr [ebp-18]
00413DF6 |. 895D D8 |mov dword ptr [ebp-28], ebx
00413DF9 |. 50 |push eax
00413DFA |. FF75 E0 |push dword ptr [ebp-20]
00413DFD |. 8D45 88 |lea eax, dword ptr [ebp-78]
00413E00 |. 56 |push esi
00413E01 |. 50 |push eax
00413E02 |. FF35 C8A04200 |push dword ptr [42A0C8]
00413E08 |. E8 3FEFFFFF |call <__decode_pointer>
00413E0D |. 59 |pop ecx
00413E0E |. FFD0 |call eax
00413E10 |. 8B5D EC |mov ebx, dword ptr [ebp-14]
00413E13 |. 83C4 1C |add esp, 1C
发现当调用__decode_pointer的后得到一个函数指针_fptrap,然后call eax会调用这个函数,而这
个函数是一个陷阱函数,会报出相应的错误,我们打开VC的crt代码来看看
先看看_woutput_l函数,找到处理格式化“%f”的代码,如下:
case _T('e'):
case _T('f'):
case _T('g'):
case _T('a'): {
/* floating point conversion -- we call cfltcvt routines */
/* to do the work for us. */
flags |= FL_SIGNED; /* floating point is signed conversion */
#ifdef POSITIONAL_PARAMETERS
if((format_type == FMT_TYPE_POSITIONAL) && (pass == FORMAT_POSSCAN_PASS))
{
_VALIDATE_RETURN(((type_pos>=0) && (type_pos<_ARGMAX)), EINVAL, -1);
#if !LONGDOUBLE_IS_DOUBLE
if (flags & FL_LONGDOUBLE)
{
STORE_ARGPTR(pos_value, e_longdouble_arg, type_pos, ch, flags)
}
else
#endif /* !LONGDOUBLE_IS_DOUBLE */
{
STORE_ARGPTR(pos_value, e_double_arg, type_pos, ch, flags)
}
break;
}
#endif /* POSITIONAL_PARAMETERS */
text.sz = buffer.sz; /* put result in buffer */
buffersize = BUFFERSIZE;
/* compute the precision value */
if (precision < 0)
precision = 6; /* default precision: 6 */
由于precision的初始值为-1,所以程序会把它设为一个默认精度6,然后再往下执行
tmp=va_arg(argptr, _CRT_DOUBLE);
00406B77 mov ecx,dword ptr [ebp+14h]
00406B7A add ecx,8
00406B7D mov dword ptr [ebp+14h],ecx
00406B80 mov edx,dword ptr [ebp+14h]
00406B83 mov eax,dword ptr [edx-8]
00406B86 mov ecx,dword ptr [edx-4]
00406B89 mov dword ptr [tmp],eax
00406B8F mov dword ptr [ebp-48Ch],ecx
#ifdef POSITIONAL_PARAMETERS
}
else
{
/* Will get here only for pass == FORMAT_OUTPUT_PASS because
pass == FORMAT_POSSCAN_PASS has a break Above */
va_list tmp_arg;
_VALIDATE_RETURN(((type_pos>=0) && (type_pos<_ARGMAX)), EINVAL, -1);
_ASSERTE(pass == FORMAT_OUTPUT_PASS);
tmp_arg = pos_value[type_pos].arg_ptr;
tmp=va_arg(tmp_arg, _CRT_DOUBLE);
}
#endif /* POSITIONAL_PARAMETERS */
/* Note: assumes ch is in ASCII range */
/* In safecrt, we provide a special version of _cfltcvt which internally calls printf (see safecrt_output_s.c) */
#ifndef _SAFECRT_IMPL
_cfltcvt_l(&tmp.x, text.sz, buffersize, (char)ch, precision, capexp, _loc_update.GetLocaleT());
00406B95 lea ecx,[ebp-40h]
00406B98 call _LocaleUpdate::GetLocaleT (404F60h)
00406B9D push eax
00406B9E mov edx,dword ptr [ebp-2Ch]
00406BA1 push edx
00406BA2 mov eax,dword ptr [ebp-30h]
00406BA5 push eax
00406BA6 movsx ecx,byte ptr [ebp-454h]
00406BAD push ecx
00406BAE mov edx,dword ptr [ebp-44h]
00406BB1 push edx
00406BB2 mov eax,dword ptr [ebp-4]
00406BB5 push eax
00406BB6 lea ecx,[tmp]
00406BBC push ecx
00406BBD mov edx,dword ptr [__cfltcvt_tab+18h (457274h)]
00406BC3 push edx
00406BC4 call _decode_pointer (40D1A0h)
00406BC9 add esp,4
00406BCC call eax
00406BCE add esp,1Ch
可以看到在源代码中调用_cfltcvt_l函数的时候其实调用一个宏,展开就是
00406BBD mov edx,dword ptr [__cfltcvt_tab+18h (457274h)]
00406BC3 push edx
00406BC4 call _decode_pointer (40D1A0h)
00406BC9 add esp,4
00406BCC call eax
00406BCE add esp,1Ch
意味着_cfltcvt_l函数指针其实是存放在一个叫做__cfltcvt_tab的表中。。。
我们再打开__cfltcvt_tab表看看:
/*-
* ... table of (model-dependent) code pointers ...
*
* Entries all point to _fptrap by default,
* but are changed to point to the appropriate
* routine if the _fltused initializer (_cfltcvt_init)
* is linked in.
*
* if the _fltused modules are linked in, then the
* _cfltcvt_init initializer sets the entries of
* _cfltcvt_tab.
-*/
PFV _cfltcvt_tab[10] = {
_fptrap, /* _cfltcvt */
_fptrap, /* _cropzeros */
_fptrap, /* _fassign */
_fptrap, /* _forcdecpt */
_fptrap, /* _positive */
_fptrap, /* _cldcvt */
_fptrap, /* _cfltcvt_l */
_fptrap, /* _fassign_l */
_fptrap, /* _cropzeros_l */
_fptrap /* _forcdecpt_l */
};
void __cdecl _initp_misc_cfltcvt_tab()
{
int i;
for (i = 0; i < _countof(_cfltcvt_tab); ++i)
{
_cfltcvt_tab[i] = (PFV)_encode_pointer(_cfltcvt_tab[i]);
}
}
源码中的注释说得很清楚了。。。Entries all point to _fptrap by default,
but are changed to point to the appropriate,
routine if the _fltused initializer (_cfltcvt_init) is linked in.
我们从源代码得到提示,肯定是因为这个_cfltcvt_tab没有得到初始化,所以TestFloat.exe执行的时候会执行到
_fptrap,如果初始化了就肯定能正确执行_cfltcvt_l函数。
我们用ollydbg 查看内存地址42A0C8(__decode_pointer的函数值)
0042A0B0 >1E6E1EF5
0042A0B4 1E6E1EF5
0042A0B8 1E6E1EF5
0042A0BC 1E6E1EF5
0042A0C0 1E6E1EF5
0042A0C4 1E6E1EF5
0042A0C8 1E6E1EF5
0042A0CC 1E6E1EF5
0042A0D0 1E6E1EF5
0042A0D4 1E6E1EF5
10个地址全部相同,可以肯定这就是经过 _encode_pointer 编码过的 _fptrap 函数地址。
知道了发生了什么事,那我们就要找出为什么会这样,是什么原因导致没有初始化这个 _cfltcvt_tab 表,
我们搜索CRT代码,找到初始化_cfltcvt_tab表的代码在
int __cdecl _cinit (
int initFloatingPrecision
)
{
int initret;
/*
* initialize floating point package, if present
*/
#ifdef CRTDLL
_fpmath(initFloatingPrecision);
#else /* CRTDLL */
if (_FPinit != NULL &&
_IsNonwritableInCurrentImage((PBYTE)&_FPinit))
{
(*_FPinit)(initFloatingPrecision);
}
_initp_misc_cfltcvt_tab();
#endif /* CRTDLL */
/*
* do initializations
*/
initret = _initterm_e( __xi_a, __xi_z );
if ( initret != 0 )
return initret;
#ifdef _RTC
atexit(_RTC_Terminate);
#endif /* _RTC */
/*
* do C++ initializations
*/
_initterm( __xc_a, __xc_z );
#ifndef CRTDLL
/*
* If we have any dynamically initialized __declspec(thread)
* variables, then invoke their initialization for the thread on
* which the DLL is being loaded, by calling __dyn_tls_init through
* a callback defined in tlsdyn.obj. We can't rely on the OS
* calling __dyn_tls_init with DLL_PROCESS_ATTACH because, on
* Win2K3 and before, that call happens before the CRT is
* initialized.
*/
if (__dyn_tls_init_callback != NULL &&
_IsNonwritableInCurrentImage((PBYTE)&__dyn_tls_init_callback))
{
__dyn_tls_init_callback(NULL, DLL_THREAD_ATTACH, NULL);
}
#endif /* CRTDLL */
return 0;
}
关键是这里:
if (_FPinit != NULL &&
_IsNonwritableInCurrentImage((PBYTE)&_FPinit))
{
(*_FPinit)(initFloatingPrecision);
}
FPInit会初始化这个表。。。于是我们又跟踪TestFloat.exe中的cinit的地方,
发现由于 _IsNonwritableInCurrentImage 的返回值为FALSE,所以导致了不初始化 _cfltcvt_tab 表。
_IsNonwritableInCurrentImage函数的原型是:
BOOL __cdecl _IsNonwritableInCurrentImage(
PBYTE pTarget
)
功能是:判断目标地址pTarget是否在当前PE的Image范围内,并且判断其所属的Section是否为不可写,
如果为不可写则返回 TRUE,否则返回 FALSE
_FPinit在.rdata段中,这个段默认情况下就是只读段,不允许写的。。。其中包含了一些函数指针数据,我想
MS可能是出于安全性的考虑,所以会调用_IsNonwritableInCurrentImage进行判断吧。
致此,我们程序的根本错误原因就很明朗了,就是因为.rdata的段属性包含
Writeable属性 ,
我们用LordPE打开TestFloat.exe看.rdata的属性,果然其含有 Writeable属性 ,验证了我的想法是正确的。
我们将其 Writeable属性 去掉,程序就不会Crash了。。。
===================================华丽的分割线===================================
我们还可以写一个小程序来验证我的结论:
#include "stdafx.h"
#include <stdio.h>
int _tmain(int argc, _TCHAR* argv[])
{
float f;
wchar_t buf[256];
f = 3.14159;
_snwprintf(buf, sizeof(buf), L"PI * 2 = %f", f + f);
return 0;
}
这个代码可以正确运行,当我们加入一句设置.rdata段函数为可写时程序就会出错,如下所示:
#include "stdafx.h"
#include <stdio.h>
#pragma comment(linker, "/Section:.rdata,RW")
int _tmain(int argc, _TCHAR* argv[])
{
float f;
wchar_t buf[256];
f = 3.14159;
_snwprintf(buf, sizeof(buf), L"PI * 2 = %f", f + f);
return 0;
}
附上一个修复的exe
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!
上传的附件: