OJ安全
OJ安全问题主要是两个方面,一个是基于Web的安全问题,包括解决SQL注入、后端过滤等问题,这个不在本文讨论的范围之内,本文着重讨论一下第二个方面的问题:由用户提交恶意代码导致的安全问题。
恶意代码
由于OJ系统的特殊性,准许在服务器运行用户的提交的代码,那么如果代码里包含关机操作、删除文件、Socket通信等与算法无关的代码则很容易使服务器或者数据库出现问题,甚至直接崩溃。
解决方法
删除头文件
这一步是初步过滤,删除一些用不到的头文件,最典型的就是`Windows.h`,还有`WinBase.h`等,这些文件直接在编译器安装目录下删除就行了,但是一定要搞清楚不要删错了,因为很多文件之间有很强的依赖关系。基于这个曾经想过直接在用户提交的代码里面搜索这些头文件,如果搜到直接就用空字符串替换,但是这并没有什么卵用,因为C/C++的宏定义太强大,可以轻松绕过很多检查。
注释掉一些函数
有些头文件可以直接删掉,但有些头文件就不能删了,比如`stdio.h`,这个肯定无法删除,但是这个头文件里有`remove`、`fopen`等操作文件的函数,依然不能让用户使用,所以只能在注释掉这些函数。然而到最后,你会发现这依然没有什么卵用,因为用户可以直接把头文件帖到代码中!
作业对象
那么到底该怎么办呢,在Linux平台下,有`ptrace`可以维系父子进程之间的关系,但是Windows下没有这种现成的函数,但是好在Windows有作业对象(Job Object)这个东西,这个东西可以将进程组合在一起,并且创建一个“沙箱”,以便限制进程能够进行的操作!这不正是我们想要的吗。我们可以把用户提交的解题程序进程放入一个作业中,从而限制它的操作,使它运行时间受限,运行内存受限,访问权限受限等等,下面就简单介绍下如何利用作业对象限制进程的操作。
首先我们通过`CreateJobObject`创建一个作业对象:
1 HANDLE hJob = CreateJobObject(NULL,NULL);
接着我们就要加上针对作业进程的限制,进程创建后,通常需要设置一个沙箱(设置一些限制),以便限制作业中的进程能够进行的操作。可以给一个作业加上若干不同类型的限制:
- 基本限制和扩展基本限制,用于防止作业中的进程垄断系统的资源。
- 基本的UI限制,用于防止作业中的进程改变用户界面。
- 安全性限制,用于防止作业中的进程访问保密资源(文件、注册表子关键字等)。
首先,我们需要加上时间内存等限制:
1 JOBOBJECT_BASIC_LIMIT_INFORMATION Job_Limit ; 2 memset(&Job_Limit, 0, sizeof(Job_Limit)) ; 3 Job_Limit.LimitFlags = JOB_OBJECT_LIMIT_PROCESS_TIME | 4 JOB_OBJECT_LIMIT_WORKINGSET | 5 JOB_OBJECT_LIMIT_ACTIVE_PROCESS ; 6 Job_Limit.PerProcessUserTimeLimit.QuadPart = 10000*1000*3; //子进程执行时间ns(1s=10^9ns) 7 Job_Limit.MinimumWorkingSetSize = 1; 8 Job_Limit.MaximumWorkingSetSize = 1024*1024*10;//10MB 9 Job_Limit.ActiveProcessLimit = 1; 10 SetInformationJobObject(hJob, JobObjectBasicLimitInformation, &Job_Limit, sizeof(Job_Limit));
其中`PerProcessUserTimeLimit`限制每个进程的运行时间,`WorkingSetSize`设置作业中每个进程的工作集大小,`Job_Limit.ActiveProcessLimit = 1;`则限制每个作业中只能由一个进程在运行。
接着,我们来加上UI限制,比如不能让进程关机,读写剪贴板等:
1 JOBOBJECT_BASIC_UI_RESTRICTIONS Job_UI_Limit; 2 Job_UI_Limit.UIRestrictionsClass = JOB_OBJECT_UILIMIT_NONE; 3 4 Job_UI_Limit.UIRestrictionsClass |= JOB_OBJECT_UILIMIT_EXITWINDOWS | //阻止进程注销,关机,重启或断开系统电源 5 JOB_OBJECT_UILIMIT_READCLIPBOARD | //阻止进程读取剪贴板中的内容。 6 JOB_OBJECT_UILIMIT_WRITECLIPBOARD | //阻止进程清除剪贴板中的内容。 7 JOB_OBJECT_UILIMIT_SYSTEMPARAMETERS | //阻止进程更改系统参数。 8 JOB_OBJECT_UILIMIT_DISPLAYSETTINGS | //进程更改显示设置。 9 JOB_OBJECT_UILIMIT_GLOBALATOMS | //为作业指定其专有的全局原子表,并限定作业中的进程只能访问此作业的表。 10 JOB_OBJECT_UILIMIT_DESKTOP | //阻止进程创建或切换桌面。 11 JOB_OBJECT_UILIMIT_HANDLES ; //阻止作业中的进程使用同一个作业外部的进程所创建的用户对象( 如HWND) 。 12 SetInformationJobObject(hJob, JobObjectBasicUIRestrictions, &Job_UI_Limit, sizeof(Job_UI_Limit));
这些限制添加完后,我们就要创建进程了:
1 STARTUPINFO si = { sizeof(si) }; 2 PROCESS_INFORMATION pi; 3 TCHAR szCmdLine[] = TEXT("calc.exe"); 4 5 CreateProcess(NULL, szCmdLine, NULL, NULL, FALSE, CREATE_BREAKAWAY_FROM_JOB | CREATE_SUSPENDED, NULL, NULL, &si, &pi);
这里为了演示,把执行程序改为Windows自带计算器,注意这里用的是`CREATE_SUSPENDED`,这样,创建了一个新进程,但是不允许它执行任何代码。
由于本程序是从不属于作业组成部分的进程来执行的,因此子进程也不属于作业的组成部分。如果准备立即允许子进程开始执行代码,那么它将跑出沙箱,并且能够成功地执行我想限制它做的工作。因此,当创建子进程之后,在我允许它开始运行之前,我必须显式地将该进程放入我新创建的作业,方法是调用下面的代码:
`AssignProcessToJobObject(hJob, pi.hProcess)`
然后我们就可以开始执行进程,并且统计我们所需要的信息:
1 ResumeThread(pi.hThread); 2 CloseHandle(pi.hThread); 3 4 //通过WaitForSingleObject等待正在运行的工作对象,设置好允许使用时间. 5 DWORD WaitRe = WaitForSingleObject(hJob,3000); 6 if(WaitRe!=WAIT_FAILED) 7 { 8 JOBOBJECT_EXTENDED_LIMIT_INFORMATION lpJobObjectInfo; 9 DWORD lpReturnLength; 10 if(QueryInformationJobObject(hJob,JobObjectExtendedLimitInformation,&lpJobObjectInfo,sizeof(lpJobObjectInfo),&lpReturnLength)) 11 { 12 printf("Memory:%dKb\n",lpJobObjectInfo.PeakProcessMemoryUsed/1024); 13 DWORD ExitCode=0; 14 //TerminateJobObject(Job,0); 15 if(GetExitCodeProcess(pi.hProcess,&ExitCode)) 16 { 17 printf("ExitCode:%d\n",ExitCode); 18 } 19 } 20 } 21 else 22 { 23 printf("WaitForSingleObject [Error:%d]\n",GetLastError()); 24 } 25 TerminateJobObject(hJob,0);//exit 26 27 CloseHandle(pi.hProcess); 28 CloseHandle(hJob);
当然了,这其中还涉及到子进程输入输出的问题,就不在这里讨论了。
访问控制列表
但是读到这里,你可能会想,好像用户提交的代码仍然可以操作服务器文件系统啊,没错,因为我们并没有对文件权限进行限制。这个时候就需要用到Windows系统的账户和访问控制列表了。
在服务器上新建一个帐户,要特别注意不能使用控制面板新建,而应该用管理——>系统工具新建,因为新建的帐户要求安全,而且不应出现在Windows系统登录界面上。对于新帐户的权限,设置其为允许登录,但是对于所有的驱动器或文件,都拒绝其读写权限。最后,新建一个文件夹,这个文件夹对新建的帐户放开读写权限,而且,将用户程序标准输出重定向到这个文件夹里。另外,对于服务器中的数据文件,放开该用户的读权限。用新建的帐号来运行用户程序。用户程序只能写指定的文件夹参考文献。
设置好账户后,再用指定账户身份来运行子进程,具体如何操作可以参考ACM在线判题系统的安全性解决方案这篇文章。
Java Policy
上述方法都是针对C/C++代码采取的措施,那么针对Java代码,就没有这么复杂了,因为Java有自带的权限控制,只需要对指定文件夹下的Java代码设置权限,就可以保证该文件夹下的代码能安全运行。具体可参见Java沙箱的实现,里面有详细的代码实现。
小结
本文只是简略对一些处理Windows平台上的安全问题的常用手段做了一些介绍,很多细节在实现时会根据各OJ系统的具体实践有一些区别,至于Linux端的处理及一些其他的方法可以参看知乎上的这个问题:Online Judge 是如何解决判题端安全性问题的。
浙公网安备 33010602011771号