TONT 41523 为什么对结构的尺寸校验是严格的?

为了你好。

原文链接:https://blogs.msdn.microsoft.com/oldnewthing/20031212-00/?p=41523

You may have noticed that Windows as a general rule checks structure sizes strictly. For example, consider the MENUITEMINFO structure:

可能你会注意到Windows通常对结构的尺寸会进行严格校验。例如,试分析如下MENUITEMINFO结构:

typedef struct tagMENUITEMINFO {
UINT cbSize;
UINT fMask;
UINT fType;
UINT fState;
UINT wID;
HMENU hSubMenu;
HBITMAP hbmpChecked;
HBITMAP hbmpUnchecked;
ULONG_PTR dwItemData;
LPTSTR dwTypeData;
UINT cch;
#if(WINVER >= 0x0500)
HBITMAP hbmpItem; // available only on Windows 2000 and higher(限Windows2000及以上版本)
#endif
} MENUITEMINFO, *LPMENUITEMINFO;

Notice that the size of this structure changes depending on whether WINVER >= 0x0500 (i.e., whether you are targetting Windows 2000 or higher). If you take the Windows 2000 version of this structure and pass it to Windows NT 4, the call will fail since the sizes don’t match.

注意该结构的尺寸在WINVER是否大于等于0x0500时(即目标系统版本是否为Windows 2000及以上)会发生变化。如果你创建了该结构的Windows 2000版本,然后传递给Windows NT 4,则调用会失败,因为尺寸不合。

“But the old version of the operating system should accept any size that is greater than or equal to the size it expects. A larger value means that the structure came from a newer version of the program, and it should just ignore the parts it doesn’t understand.”

『可是旧版操作系统应该接受比其预期尺寸更大或相等的任何尺寸吧?更大的尺寸意味着传入的结构来自新版应用,只要忽略其无法理解的部分就好了。』

We tried that. It didn’t work.

我们试过了,这招不好使。

Consider the following imaginary sized structure and a function that consumes it. This will be used as the guinea pig for the discussion to follow:

试考虑如下虚构尺寸的结构,以及一个可以接受其传入的函数,这些将用作接下来讨论的小白鼠:

typedef struct tagIMAGINARY {
UINT cbSize;
BOOL fDance;
BOOL fSing;
#if IMAGINARY_VERSION >= 2
// v2 added new features
IServiceProvider *psp; // where to get more info(获取更多信息的途径)
#endif
} IMAGINARY;

// perform the actions you specify(执行所指定的操作)
STDAPI DoImaginaryThing(const IMAGINARY *pimg);

// query what things are currently happening(查询正在发生的事情)
STDAPI GetImaginaryThing(IMAGINARY *pimg);

First, we found lots of programs which simply forgot to initialize the cbSize member altogether.

首先,我们发现有好多应用程序干脆就忘记初始化cbSize成员了。

IMAGINARY img;
img.fDance = TRUE;
img.fSing = FALSE;
DoImaginaryThing(&img);

So they got stack garbage as their size. The stack garbage happened to be a large number, so it passed the “greater than or equal to the expected cbSize” test and the code worked. Then the next version of the header file expanded the structure, using the cbSize to detect whether the caller is using the old or new style. Now, the stack garbage is still greater than or equal to the new cbSize, so version 2 of DoImaginaryThing says, “Oh cool, this is somebody who wants to provide additional information via the IServiceProvider field.” Except of course that it’s stack garbage, so calling the IServiceProvider::QueryService method crashes.

如此一来这个结构的尺寸就是栈中的垃圾数据,而这个垃圾数据碰巧是一个大数字,所以就通过了『比预期的cbSize大或相等』的校验。然后,下一版本的头文件扩充了结构体,使用cbSize来探测调用方是在使用旧的还是新的结构。然后,那个垃圾栈值仍然比新的cbSize要大或者想等,所以版本2的DoImaginaryThing想,『哦,好耶,这肯定是个要通过IServiceProvider域提供更多信息的friends。』然而那部分(因为根本没有初始化)照旧仍是内存栈中的垃圾数据,对IServiceProvider::QueryService调用就那么炸掉了。

Now consider this related scenario:

然后再来看一个相关的场景:

IMAGINARY img;
GetImaginaryThing(&img);

The next version of the header file expanded the structure, and the stack garbage happened to be a large number, so it passed the “greater than or equal to the expected cbSize” test, so it returned not just the fDance and fSing flags, but also returned an psp. Oops, but the caller was compiled with v1, so its structure doesn’t have a psp member. The psp gets written past the end of the structure, corrupting whatever came after it in memory. Ah, so now we have one of those dreaded buffer overflow bugs.

下一版的头文件对结构进行了扩充,而内存栈垃圾数据又正好是一个大数字,恰好通过了『比预期的cbSize大或相等』的校验,所以不仅返回了fDance和fSing的flag,还返回了一个psp(译注:前文中的IServiceProvider)。但是呢,调用方是用第1版的代码进行编译的,对应的结构里并没有psp这个成员,现在这个psp就被写到了结构结尾之后的部分,损毁了内存中在它之后的任何数据。哎呀,现在我们有了一个令人生畏的缓冲区溢出bug了。

Even if you were lucky and the memory that came afterwards was safe to corrupt, you still have a bug: By the rules of COM reference counts, when a function returns an interface pointer, it is the caller’s responsibility to release the pointer when no longer needed. But the v1 caller doesn’t know about this psp member, so it certainly doesn’t know that it needs to be psp->Release()d. So now, in addition to memory corruption (as if that wasn’t bad enough), you also have a memory leak.

就算你足够幸运,(那个psp)后面的内存玩坏了也没关系,还是要面对另一个bug:根据COM组件手册的规定,当某个函数返回了一个interface指针时,由调用方尽义务在其不再使用时释放这个指针。但v1版调用方根本没听说过这个psp成员,所以肯定也不知道它需要被psp->Release()。好了,如今除了内存数据损毁的问题(好像这个问题还不够糟糕一样),又多一个内存泄漏的问题。

Wait, I’m not done yet. Now let’s see what happens when a program written in the future runs on an older system.

不过等等,我还没说完呢。接下来我们来看看一个成于未来的程序在旧版本系统上运行的情况。

Suppose somebody is writing their program intending it to be run on v2. They set the cbSize to the larger v2 structure size and set the psp member to a service provider that performs security checks before allowing any singing or dancing to take place. (E.g., makes sure everybody paid the entrance fee.) Now somebody takes this program and runs it on v1. The new v2 structure size passes the “greater than or equal to the v1 structure size” test, so v1 will accept the structure and Do the ImaginaryThing. Except that v1 didn’t support the psp field, so your service provider never gets called and your security module is bypassed. Now everybody is coming into your club without paying.

假设有人写了一段程序,调用了第2版的结构。写代码的人设置了cbSize为较大的v2结构的值,并设置了psp成员为一个会在做任何实际行动(比如,看看来的人买没买门票之类的事情)之前做安全检查的Service Provider。现在有人拿这个程序跑在(只支持)v1(的系统上)。新的v2结构尺寸通过了『比v1的结构大或相等』的校验,然后v1就接受了这个结构,开始运行ImaginaryThing,然而v1并不支持psp这个域,所以费尽心思撰写的Service Provider永远不会被执行,相应的安全检查也都被越过了,好似是个人都能不花钱跑进你的俱乐部里玩一般。

Now, you might say, “Well those are just buggy programs. They deserve to lose.” If you stand by that logic, then prepare to take the heat when you read magazine articles like “Microsoft intentionally designed <Product X> to be incompatible with <software from a major competitor>. Where is the Justice Department when you need them?”

到此为止你可能会说,『哎呀,那些bug代码活该在竞争中失败。』仰赖这种逻辑的话,就做好面对如下场景的准备吧:某本杂志上登出大大的标题,写着『微软故意将产品X设计为与某主要竞争对手的软件不兼容,在这种丧尽天良的事发生时天理何在?』

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注

 剩余字数 ( Characters available )

Your comment will be available after auditing.
您的评论将在通过审核后显示。

Please DO NOT add any links in your comment, otherwise it would be identified as SPAM automatically and never be audited.
请不要在评论中插入任何链接,否则将被自动归类为垃圾评论,且永远不会被提交给博主进行复审。

*