OJ的后端

内容有复制和参考。

这篇文章主要用于记录在探索评测系统Reef期间我所学的东西,以便之后查阅.

Reef预计主要支持远程评测

加一点本地评测…

结果主要的东西都特么是本地评测的.

总体架构

Reef预计将采用seccomp作为第一道安全关卡,使用多线程检测程序耗时/内存等信息.

在外层使用docker封装并再次限制资源,接入队列以能够方便的横向扩展.

seccomp

seccomp为linux系统上才有的安全技术,因此必须使用linux.在安装必要的安装包后

1
$ sudo apt install libseccomp2 libseccomp-dev seccomp

即可使用.

当然,在windows下的Jobs似乎也可以利用,但是我不太懂,微软文档写得也奇怪,而且还得是服务器版本的windows才能用.

基本使用

seccomp需要由程序主动加载.其使用方法基本为下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//g++ -g test.c -o o -lseccomp
#include <unistd.h>
#include <seccomp.h>
#include <linux/seccomp.h>

int main(void){
//初始化筛选器
scmp_filter_ctx ctx;
ctx = seccomp_init(SCMP_ACT_ALLOW);//flag指明默认通过
//添加拦截,并指明一旦拦截就将程序kill掉.
seccomp_rule_add(ctx, SCMP_ACT_KILL, SCMP_SYS(execve), 0);
//将规则加载注入.
seccomp_load(ctx);

char * filename = "/bin/sh";
char * argv[] = {"/bin/sh",NULL};
char * envp[] = {NULL};
write(1,"i will give you a shell\n",24);
//程序将会在此行崩溃.
syscall(59,filename,argv,envp);//execve
return 0;
}

seccomp_init是初始化的过滤状态,这里用的是SCMP_ACT_ALLOW,表示默认允许所有的syscacll.如果初始化状态为SCMP_ACT_KILL,则表示默认不允许所有的syscall.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* Kill the process
*/
#define SCMP_ACT_KILL 0x00000000U
/**
* Throw a SIGSYS signal
*/
#define SCMP_ACT_TRAP 0x00030000U
/**
* Return the specified error code
*/
#define SCMP_ACT_ERRNO(x) (0x00050000U | ((x) & 0x0000ffffU))
/**
* Notify a tracing process with the specified value
*/
#define SCMP_ACT_TRACE(x) (0x7ff00000U | ((x) & 0x0000ffffU))
/**
* Allow the syscall to be executed after the action has been logged
*/
#define SCMP_ACT_LOG 0x7ffc0000U
/**
* Allow the syscall to be executed
*/
#define SCMP_ACT_ALLOW 0x7fff0000U

规则添加

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* Add a new rule to the filter
* @param ctx the filter context
* @param action the filter action
* @param syscall the syscall number
* @param arg_cnt the number of argument filters in the argument filter chain
* @param ... scmp_arg_cmp structs (use of SCMP_ARG_CMP() recommended)
*
* This function adds a series of new argument/value checks to the seccomp
* filter for the given syscall; multiple argument/value checks can be
* specified and they will be chained together (AND&#039;d together) in the filter.
* If the specified rule needs to be adjusted due to architecture specifics it
* will be adjusted without notification. Returns zero on success, negative
* values on failure.
*
*/
int seccomp_rule_add(scmp_filter_ctx ctx,
uint32_t action, int syscall, unsigned int arg_cnt, ...);

seccomp_rule_add(ctx, SCMP_ACT_KILL, SCMP_SYS(execve), 0);,arg_cnt为0,表示直接限制execve,不管他什么参数.

如果arg_cnt不为0,那arg_cnt表示后面限制的参数的个数,也就是只有调用execve,且参数满足要求时,才会拦截syscall.

1
seccomp_rule_add(ctx, SCMP_ACT_KILL, SCMP_SYS(write),1,SCMP_A2(SCMP_CMP_EQ,0x10));//第2(从0)个参数等于0x10
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
/**
* Specify an argument comparison struct for use in declaring rules
* @param arg the argument number, starting at 0
* @param op the comparison operator, e.g. SCMP_CMP_*
* @param datum_a dependent on comparison
* @param datum_b dependent on comparison, optional
*/
#define SCMP_CMP(...) ((struct scmp_arg_cmp){__VA_ARGS__})

/**
* Specify an argument comparison struct for argument 0
*/
#define SCMP_A0(...) SCMP_CMP(0, __VA_ARGS__)

/**
* Specify an argument comparison struct for argument 1
*/
#define SCMP_A1(...) SCMP_CMP(1, __VA_ARGS__)

/**
* Specify an argument comparison struct for argument 2
*/
#define SCMP_A2(...) SCMP_CMP(2, __VA_ARGS__)

/**
* Specify an argument comparison struct for argument 3
*/
#define SCMP_A3(...) SCMP_CMP(3, __VA_ARGS__)

/**
* Specify an argument comparison struct for argument 4
*/
#define SCMP_A4(...) SCMP_CMP(4, __VA_ARGS__)

/**
* Specify an argument comparison struct for argument 5
*/
#define SCMP_A5(...) SCMP_CMP(5, __VA_ARGS__)



/**
* Comparison operators
*/
enum scmp_compare {
_SCMP_CMP_MIN = 0,
SCMP_CMP_NE = 1, /**< not equal */
SCMP_CMP_LT = 2, /**< less than */
SCMP_CMP_LE = 3, /**< less than or equal */
SCMP_CMP_EQ = 4, /**< equal */
SCMP_CMP_GE = 5, /**< greater than or equal */
SCMP_CMP_GT = 6, /**< greater than */
SCMP_CMP_MASKED_EQ = 7, /**< masked equality */
_SCMP_CMP_MAX,
};

/**
* Argument datum
*/
typedef uint64_t scmp_datum_t;

/**
* Argument / Value comparison definition
*/
struct scmp_arg_cmp {
unsigned int arg; /**< argument number, starting at 0 */
enum scmp_compare op; /**< the comparison op, e.g. SCMP_CMP_* */
scmp_datum_t datum_a;
scmp_datum_t datum_b;
};

ctx的内容可以使用函数dump出来,之后可以直接使用prctl命令相关直接载入,方便使用?

seccomp调试

使用如下命令导出所有可能的命令

1
2
3
4
file=syscall-names.h
echo "static const char *syscall_names[] = {" > $file
echo "#include <sys/syscall.h>" | cpp -dM | grep &#039;^#define __NR_&#039; | LC_ALL=C sed -r -n -e &#039;s/^\#define[ \t]+__NR_([a-z0-9_]+)[ \t]+([0-9]+)(.*)/ [\2] = "\1",/p&#039; >> $file
echo "};" >> $file

使用如下代码导出一段代码所需要的权限.遵循最小权限原则,试验代码运行所需要的最少权限.

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
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
#define __USE_GNU 1
#define _GNU_SOURCE 1
#include <signal.h>
#include <sys/prctl.h>
#include <linux/types.h>
#include <linux/filter.h>
#include <linux/seccomp.h>
#include <seccomp.h>
#include <unistd.h>
#include <string.h>
#include <stddef.h>
#include "syscall-names.h"
#if defined(__i386__)
#define REG_RESULT REG_EAX
#define REG_SYSCALL REG_EAX
#define REG_ARG0 REG_EBX
#define REG_ARG1 REG_ECX
#define REG_ARG2 REG_EDX
#define REG_ARG3 REG_ESI
#define REG_ARG4 REG_EDI
#define REG_ARG5 REG_EBP
#elif defined(__x86_64__)
#define REG_RESULT REG_RAX
#define REG_SYSCALL REG_RAX
#define REG_ARG0 REG_RDI
#define REG_ARG1 REG_RSI
#define REG_ARG2 REG_RDX
#define REG_ARG3 REG_R10
#define REG_ARG4 REG_R8
#define REG_ARG5 REG_R9
#endif
#ifndef SYS_SECCOMP
#define SYS_SECCOMP 1
#endif

const char *const msg="system call invalid: ";

static void write_uint(char *buf, unsigned int val)
{
int width = 0;
unsigned int tens;
if (val == 0) {
strcpy(buf, "0");
return;
}
for (tens = val; tens; tens /= 10)
++ width;
buf[width] = &#039;\0&#039;;
for (tens = val; tens; tens /= 10)
buf[--width] = (char) (&#039;0&#039; + (tens % 10));
}
static void helper(int nr, siginfo_t *info, void *void_context) {
char buf[255];
ucontext_t *ctx = (ucontext_t *)(void_context);
unsigned int syscall;
if (info->si_code != SYS_SECCOMP)
return;
if (!ctx)
return;
syscall = (unsigned int) ctx->uc_mcontext.gregs[REG_SYSCALL];
strcpy(buf, msg);
if (syscall < sizeof(syscall_names)) {
strcat(buf, syscall_names[syscall]);
strcat(buf, "(");
}
write_uint(buf + strlen(buf), syscall);
if (syscall < sizeof(syscall_names))
strcat(buf, ")");
strcat(buf, "\n");
write(STDOUT_FILENO, buf, strlen(buf));
_exit(1);
}
static int install_helper() {
struct sigaction act;
sigset_t mask;
memset(&act, 0, sizeof(act));
sigemptyset(&mask);
sigaddset(&mask, SIGSYS);
act.sa_sigaction = &helper;
act.sa_flags = SA_SIGINFO;
if (sigaction(SIGSYS, &act, NULL) < 0) {
perror("sigaction");
return -1;
}
if (sigprocmask(SIG_UNBLOCK, &mask, NULL)) {
perror("sigprocmask");
return -1;
}
return 0;
}

#include <stdio.h>
int main(){
if(install_helper()){
printf("install helper failed");
return 1;
}

scmp_filter_ctx ctx = NULL;
ctx = seccomp_init(SCMP_ACT_ALLOW);

seccomp_rule_add(ctx, SCMP_ACT_TRAP, SCMP_SYS(execve), 0);
seccomp_load(ctx);
seccomp_release(ctx);
fprintf(stdout, "something to stdout\n");
char * filename = "/bin/sh";
char * argv[] = {"/bin/sh",NULL};
char * envp[] = {NULL};
write(1,"i will give you a shell\n",24);
syscall(59,filename,argv,envp);//execve

return 0;
}

注入程序

刚刚提到了seccomp必须由程序主动加载,因此需要有一个办法将代码注入到用户的代码中.

等待完成

其他语言

这玩意貌似至少能方便的用在c系语言,java和python上.

Docker

准备使用Docker重构整个OJ

多阶段构建

使用Docker,将OJ分解为多个阶段来构建.

  1. 前端构建
  2. 后端构建
  3. 运行环境构建与代码整合
  4. nginx构建

命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#...

# 使用php作为基础,指明该构建阶段为codeisland
FROM php:alpine as codeisland
# 在容器打包阶段安装数据库驱动
RUN docker-php-ext-install pdo pdo_pgsql

ARG PATH=/app/laravel

# 从其他阶段复制代码到本阶段
COPY --from=DELETED /app/ ${PATH}

# 执行其他初始化命令
# [DELETED]

# 指明容器的工作路径
WORKDIR ${PATH}

#...

容器协调

使用Docker-compose来协调各个容器的关系.

  • 数据库容器
  • redis容器
  • 网站后端容器
  • 评测器容器
  • 评测代理容器
  • nginx容器

限制评测器容器的资源消耗的例子

1
2
3
4
5
deploy:
resources:
limits:
cpus: &#039;0.50&#039;
memory: 1024M

网络

…学校网关的登录状态根本没法维持,卡死.


OJ的后端
https://blog.chenc.me/2019/08/21/backend-of-oj/
作者
CC
发布于
2019年8月22日
许可协议