diamond_fulldiamonddiamond_halfdiamond_eurosearch-iconmenuchat-iconclose-iconenvelope-iconsmartphone-call-icon

Blog & News

CacheWarp: Dropping one write to take over AMD-SEV

preview-image for Logo of the CacheWarp attack

On 2023-11-14 the CISPA Helmholtz Center for Information Security published a new Attack on AMD-SEV called CacheWarp CVE-2023-20592 , in which I am one of the original authors. This attack allows a malicious hypervisor to drop memory writes on an encrypted Virtual Machine using the invd instruction. Due to the difficulty of the setup, a feasible attack should only drop memory once to achieve its goal. In this article, we examine how one memory drop is enough to break openssh and sudo to completely hijack the victim Virtual Machine.

Preliminaries

AMD-SEV

AMD-SEV, or AMD Secure Encrypted Virtualization, is a hardware-based security feature that enables encryption of virtual machines' memory, theoretically safeguarding data from unauthorized access even if the hypervisor is compromised. It helps protect sensitive information within cloud environments by encrypting each virtual machine’s memory, enhancing overall system security.

Attack scenario

In the attack scenario, we assume the attacker to be a malicious hypervisor, who wants to break into a guest Virtual Machine running on AMD-SEV. Using CacheWarp the attacker can leverage the invd instruction to selectively drop a memory write. Using this primitive the attacker wants to break into the system by first logging into the machine using openssh and then escalating their privileges using sudo.

Attack Detail: Dropping a memory write

In our context, a memory write is when a program attempts to write any value to a specific address. A write to a register cannot be attacked by CacheWarp. Due to the complexity of the CacheWarp attack, it is best to find attacks that only require one dropping a single write. The specifics of how the write drop is accomplished are out of the scope of this article and can be found in the original Research Paper [1].

Attacks

The sudo attack is a lot simpler than the openssh attack, therefore we analyze this one first.

Privilege escalation - Sudo - keep UID=0

sudo allows a user to execute commands with administrative privileges but contains a few checks to determine whether a user is authorized to do so. By dropping a specific write we can trick sudo into thinking that we are already the root user, bypassing any further checks. The attack targets the get_user_info function, which collects various information about the current user. We abuse that the root user has UID=0 and the struct user_details is initialized with zero values.

static char ** get_user_info(struct user_details *ud) {
    // ...
    ud->uid = getuid(); // <-- victim
    // ...
}

When we drop the write in the line ud->uid = getuid(), ud->uid stays 0. The next time sudo checks, it will see UID=0 and believe we are the root user and executing our requested command with administrative privileges without any further checks.

Initial Access - OpenSSH - Leveraging TimeWarp

The attack on openssh targets the control flow of the program, using a primitive called Timewarp to trick the program into checking if (real_password == real_password) return authorized

Challenge - Compiler Optimizations

While the sudo exploit conveniently has one variable that lets us pass all checks, it is quite difficult to find such convenient write-drops in the wild. One big reason is that most programs are compiled using GCC with the compiler flag -O2 which optimizes compiled C-Code. One effect of this optimization is that local variables, where possible, are stored in registers instead of in memory. Since CacheWarp cannot attack registers, most lines, like the one below are probably not vulnerable.

void foo(){
    // ...
    authorized = 0;  // <-- Not vulnerable
    // ...
}

While there are exceptions, as a general rule CacheWarp can only be used on global variables and structs since these are memory writes and not optimized into registers. In the sudo case, we were very lucky that the UID was stored in the user_details struct.

TimeWarp - Using knowledge from the future and then going back in time

To expand the attack surface, it is necessary to look beyond for new ideas: Programs use memory for more than just storing global variables and structs One such use-case is using the stack to keep track of function return addresses. Consider the following example:

void foo(){
    //...
}
int main(){
    puts("Before foo");
    foo();
    puts("After foo"); // Return address
}

Before entering the foo function, the program stores the return address of puts("After foo") on the stack. When foo finishes, the program can continue where it left off.

Stack before foo()Stack after calling foo()
uninitializedmain+33 (Address of “After foo”)

The Stack is part of memory and therefore vulnerable to CacheWarp. In the following example, we can leverage this behavior to manipulate control flow into an impossible state:

#include <stdio.h>
int return0(){
    return 0;
}
int return1(){
    return 1;
}
int main(){
    if (return0() == 1){ // <- Stale return address
        puts("You are a time-warping Wizard!");
    }
    return1(); // <- Victim
    puts("After return1"); // <- Correct return address
}

The x86-Assembly of the main function looks like this:

main+0:	    endbr64 
main+4:	    push   rbp
main+5:	    mov    rbp,rsp
main+8:	    mov    eax,0x0
main+13:	call   0x555555555149 return0
main+18:	cmp    eax,0x1                # <- Stale return address
main+21:	jne    0x55555555518d main+38
main+23:	lea    rax,[rip+0xe7f]        # 0x555555556004 ("You are a time-warping ...")
main+30:	mov    rdi,rax
main+33:	call   0x555555555050 puts@plt
main+38:	mov    eax,0x0
main+43:	call   0x555555555158 return1 # <- Victim
main+48:	mov    eax,0x0                # <- Correct return address
main+53:	pop    rbp
main+54:	ret    

The call instruction contains an implicit write of the return address to the stack. By triggering a write drop at main+43 call return 1, we can trigger a so-called TimeWarp. This effectively returns the program to main+18 with the return value of 1, printing out “You are a time-warping wizard”. In detail, the following steps happen:

  1. When calling return0() the program writes the main+18 as the return address onto the stack.
  2. When calling return1() the program attempts to write main+48 as the new return address onto the stack.
  3. Writing of the new return address is blocked, and the old return address will be used when the function return1 returns the program to main+18.
Stack after call return0after call return 1Dropping the write
main+18main+48main+18 (stale return address)

Exploiting OpenSSH password Login

Using this TimeWarp primitive, we can now attack the password login of OpenSSH. The sys_auth_passwd function checks whether the user has entered the correct password. When the function returns 1 the user is authenticated.

// Returns 1 if user authenticated 0 else
int sys_auth_passwd(struct ssh *ssh, const char *password) {
    [...]
    char *encrypted_password, *salt = NULL;
    char *pw_password = shadow_pw(pw); // <- stale return address
    // After the TimeWarp: pw_password = xcrypt(password, salt)
    [...]
    if (authctxt->valid && pw_password[0] && pw_password[1])
        salt = pw_password;
    encrypted_password = xcrypt(password, salt); // <- victim
    return encrypted_password != NULL && strcmp(encrypted_password, pw_password) == 0;
}

Using TimeWarp, we only need to target the xcrypt call, such that we return back to the line char *pw_password = NULL. This reuses the return value of xcrypt making it such that:

pw_password = xcrypt(password, salt);
encrypted_password = xcrypt(password, salt);
// The following check passes since both strings are the same
strcmp(encrypted_password, pw_password) == 0 // -> User gets authenticated

An observant reader may ask whether the salt value makes any difference. The answer is that it does not make a differenc since the first two characters of a password hash are always $y. Given that xcrypt only uses the first two characters of a salt, the result of both xcrypt calls will stay the same.

Summary

In this article, we demonstrate, how a single write drop CacheWarp, can be leveraged to completely take over a system using openssh and sudo.

References

[1] Zhang, R., Gerlach, L., Weber, D., Hetterich, L., Lü, Y., Kogler, A., & Schwarz, M. (2024). CacheWarp: Software-based Fault Injection using Selective State Reset. 33rd USENIX Security Symposium (USENIX Security 24). Paper

~ Youheng Lü

Free Consultation