如何保证oj的安全
本人在安全方面的知识比较匮乏,本文只是对代码运行时如何保证oj的安全做分析。
两种方案
-
ptrace
顾名思义这个函数时对进程进行跟踪的,它是提供给父进程一种跟踪检查子进程的能力,包括监视子进程的寄存器的值(这也是我用来保证oj安全的一种方法)。
-
这是一种容器的概念,一种新的虚拟化技术,但它不是虚拟机,它的启动是虚拟机无法比的。
它可以保证容器之间互不影响,因为本身具有虚拟机的概念,所以,我们oj的程序可以丢在里面随便跑,
即使执行rm -rf /*
这种骚操作也不会影响真正的物理机。(但本文所说的不是采用这种方式)
ptrace
-
ptrace 与 gdb
或许说道ptrace让人很懵(我第一次听说是这样),但是说到gdb就再熟悉不过了吧,其实gdb的调试原理就是基于ptrace的。
首先gdb会和要调试的程序关联起来,就是建立跟踪关系,父进程(gdb)fork出子进程,子进程调用ptrace设置第一个参数为PTRACE_TRACEME
这样子进程(被调试的程序)收到的所有信号(不包括SIGKILL)都会被父进程捕获,此时父进程就可以去观察子进程的内存,寄存器状态甚至修改他们。 -
如何使用ptrace保证oj代码安全执行
说到这个问题首先就要讲一下系统调用的原理了。
-
什么是系统调用
简单来说就是给用户程序提供一种访问操作系统更高特权的服务的能力,
具体可以移步维基百科-系统调用 -
系统调用如何实现的
- 把系统调用号写入eax寄存器
- 提供参数
- 触发int 0x80中断(此中断就是用于系统调用的)
- 从eax获取返回值
明白了系统调用原理,现在要保证oj的代码执行安全就很容易了,由于所有的函数调用最终都会转化到系统调用上
(让我想到了计网的everthing over ip, ip over everthing),每个系统调用都有系统调用号(具体查看#include <syscall.h>
),所以我们的oj只需要去跟踪程序的系统调用是否合法,从而
保证oj的安全。原理就是:- 和用户提交的程序建立跟踪与被跟踪关系
- 监视系统调用
- 获取系统调用号
- 合法放行,不合法发送SIGKILL信号
代码框架:
int fdRes[2]; int ret1 = pipe(fdRes); pid_t pid; int res; pid = fork(); long orig_rax; chdir(workDir.c_str()); if(pid > 0) { close(fdRes[1]); while(1) { waitpid(pid,&res,0); //获取系统调用号 orig_rax = ptrace(PTRACE_PEEKUSER,pid,8*ORIG_RAX,NULL); if(orig_rax >= 0 && sysCall[orig_rax] == OK_CALL/*判断是否合法*/) printf("The child made a system call %ld\n",orig_rax); else if(orig_rax < 0){ break; }else{ cout<<"非法系统调用:"<<orig_rax<<endl; //非法系统调用,杀死进程 kill(pid,SIGKILL); break; } //跟踪系统调用 ptrace(PTRACE_SYSCALL, pid, NULL, NULL); } char buf[1024]; //读取程序运行结果 read(fdRes[0],buf,sizeof(buf)); close(fdRes[0]); runRes = buf; if(res == 0) { this->status = RUN_OK; } else { this->status = RUN_ERROR; } cout<<"运行结果:"<<runRes<<endl; } else { close(fdRes[0]); freopen("data.in", "r", stdin); //运行用户程序 todo dup2(fdRes[1], STDOUT_FILENO); dup2(fdRes[1], STDERR_FILENO); //建立跟踪关系 ptrace(PTRACE_TRACEME, 0, NULL, NULL); this->doRun(); close(fdRes[1]); exit(0); }
-
网友评论