Raymond Chen 2004年08月04日
许多人会推荐使用共享数据节作为在应用程序的多个实例之间共享数据的一种方式。这听起来是个好主意,但实际上它是一个安全漏洞。
由CreateFileMapping函数正确创建的共享内存对象是可以被保护的。它们有安全描述符,允许你指定哪些用户被允许拥有什么级别的访问权限。相比之下,任何加载了你的EXE或DLL的人都可以访问你的共享内存节。
让我用一个故意不安全的程序来演示。
拿一个scratch程序并进行以下更改:
#pragma comment(linker, “/SECTION:.shared,RWS”)
#pragma data_seg(“.shared”)
int g_iShared = 0;
#pragma data_seg()
void CALLBACK TimerProc(HWND hwnd, UINT, UINT_PTR, DWORD)
{
int iNew = g_iShared + 1;
if (iNew == 10) iNew = 0;
g_iShared = iNew;
InvalidateRect(hwnd, NULL, TRUE);
}
BOOL
OnCreate(HWND hwnd, LPCREATESTRUCT lpcs)
{
SetTimer(hwnd, 1, 1000, TimerProc);
return TRUE;
}
void
PaintContent(HWND hwnd, PAINTSTRUCT *pps)
{
TCHAR sz[2];
wsprintf(sz, TEXT(“%d”), g_iShared);
TextOut(pps->hdc, 0, 0, sz, 1);
}
继续运行这个程序。它不断地从0数到9。由于TimerProc函数从不让g_iShared超过9,wsprintf对缓冲区溢出是安全的。
或者不是?
运行这个程序。然后使用runas实用工具以不同用户身份运行这个程序的第二个副本。为了更有趣,让一个用户是管理员,另一个不是管理员。
注意计数器以双倍速度计数。这是可以预期的,因为计数器是共享的。
好的,现在关闭一个副本,然后在调试器下重新启动它。(如果你让管理员的副本自由运行,并且在调试器下运行非管理员的副本,那会更有趣。)
让两个程序都运行,然后在调试器中中断并更改变量g_iShared的值为一个非常大的数字,比如说1000000。
现在,根据你的调试器的侵入性,你可能会或可能不会看到崩溃。一些调试器是“有帮助的”,并且“取消共享”共享内存节,当你从调试器中更改它们的值时。对于调试(也许),对于我的演示(绝对是)是不好的。
这是我如何使用内置的ntsd调试器做到的。
我打开了一个命令提示符,它以我自己的身份运行(我不是管理员)。
然后我使用runas实用程序以管理员身份运行scratch程序。
我将使管理员的scratch程序副本崩溃,尽管我只是一个无聊的普通非管理员用户。
从正常的命令提示符,我输入了“ntsd scratch”在调试器下运行scratch程序。
从调试器提示符,我输入了“u TimerProc”来反汇编TimerProc函数,寻找
01001143 a300300001 mov [scratch!g_iShared (01003000)],eax
(注意:你的数字可能不同)。
然后我输入了“g 1001143”以指示调试器正常执行直到达到该指令。
当调试器中断时,我输入了“r eax=12341234;t”将eax寄存器的值更改为0x12341324,然后跟踪一个指令。
那个单步跟踪将超出范围的值写入共享内存,一秒钟后,管理员版本的程序因缓冲区溢出而崩溃。
发生了什么?
由于内存是共享的,所有正在运行的scratch程序副本都可以访问它。我只是使用调试器运行了scratch程序的副本并更改了共享内存变量的值。由于变量是共享的,管理员程序副本中的值也会改变,这导致wsprintf缓冲区溢出,从而崩溃了管理员的程序副本。
拒绝服务已经够糟糕了,但如果程序在共享内存中保留了任何有价值的东西,你真的可以做一些有趣的事情。如果有一个指针,你可以破坏指针。如果有一个字符串,你可以移除空终止符并使其变得“不可能”长,如果有人复制它而没有检查长度,可能会导致潜在的缓冲区溢出。
如果有一个带有vtable的C++对象,那么你就中了大奖!你要做的是将vtable重定向到一个虚假的vtable(你构造在共享内存节中),并在该vtable中放置一个指向你生成的代码(也进入共享内存节)的函数指针条目,该代码接管了机器。(如果启用了NX,那么攻击就更难了,但原则上仍然可能。)
即使你不能通过搞乱共享内存中的变量来触发缓冲区溢出,你仍然可以导致程序行为异常。在共享内存节上随意写入随机数字肯定会在被攻击的程序中引起“有趣”的行为。
故事的寓意:避免共享内存节。由于你不能在节上附加ACL,任何可以加载你的EXE或DLL的人都可以修改你的变量并在另一个以更高安全级别运行的程序实例中造成混乱。