Monday, February 27, 2023

Reverse Engineering Notes - Race Conditions

Race Conditions

Race Conditions - Thank you shared resources and parallelism !

A race condition is a situation in which the behavior of a program depends on the order and timing of events, which are not guaranteed or predictable. In other words, it occurs when multiple threads or processes try to access a shared resource at the same time and the result of the execution depends on the order of the operations. This can lead to unexpected behavior, such as data corruption or access violations.

Double Fetch

A double fetch vulnerability occurs when a program fetches the same resource twice without adequate security checks in between. This can allow an attacker to modify the resource between the two fetches, leading to security vulnerabilities such as privilege escalation or denial of service attacks.

Time of Check, Time of Use (TOCTOU)

A Time of Check, Time of Use (TOCTOU) vulnerability is a type of race condition in which the state of a resource changes between the time it is checked and the time it is used. This can lead to unexpected or malicious behavior, such as privilege escalation or denial of service attacks.

Root Causes

 The root cause of race conditions is the presence of shared resources that can be accessed by multiple threads or processes simultaneously, without proper synchronization. These shared resources can include both volatile and non-volatile memory, as well as other system resources like files, sockets, and hardware devices.

In a multi-threaded program, different threads can execute concurrently and access shared memory locations, such as global variables or heap-allocated memory, without proper synchronization. Similarly, in a multi-process program, different processes can access shared resources, such as files or sockets, without proper synchronization.

When multiple threads or processes access the same shared resource without proper synchronization, the order of access and updates can become unpredictable, leading to race conditions. For example, if two threads try to increment the value of a shared variable simultaneously, it's possible that both threads read the same value before incrementing it, resulting in a lost update.

In summary, race conditions can occur whenever there are shared resources that can be accessed concurrently by multiple threads or processes, regardless of whether those resources are stored in volatile or non-volatile memory. To prevent race conditions, it's important to use proper synchronization mechanisms, such as locks, semaphores, or atomic operations, to ensure that only one thread or process can access a shared resource at a time.

examples:

  • 2+ clients talking to the same server
  • 2+ tabs executing javascript in the same browser
  • 2+ userspace threads/apps executing system calls in the same OS
  • 2+ OSes running in the same hypervisor

logic flow

 Here are some general steps you can follow to test for race conditions using radare2:

  1. Identify shared resources: Use radare2 to analyze the binary code and identify which memory locations or system resources are shared among multiple threads or processes. This can include global variables, heap-allocated memory, or files and sockets that are accessed by multiple threads or processes.

  2. Analyze access patterns: Use radare2 to analyze the assembly code of the program and identify how shared resources are accessed by different threads or processes. Look for patterns where multiple threads or processes access the same resource without proper synchronization.

  3. Simulate concurrent execution: Use radare2 to simulate concurrent execution of the program by setting breakpoints at relevant locations and using the dcb command to create multiple threads or processes. This can help you identify potential race conditions by observing how shared resources are accessed in different execution contexts.

  4. Test synchronization mechanisms: Use radare2 to test different synchronization mechanisms, such as locks or semaphores, to see how they affect the behavior of the program. You can use the dcb command to create multiple threads or processes that use different synchronization mechanisms and observe how they interact with shared resources.

     

Here are some examples of how these concepts can be used in C code:

Race Condition example:

c
#include <stdio.h> #include <pthread.h> int global_variable = 0; void* thread_func(void* arg){ for (int i = 0; i < 1000000; i++){ global_variable++; } return NULL; } int main(){ pthread_t thread1, thread2; pthread_create(&thread1, NULL, thread_func, NULL); pthread_create(&thread2, NULL, thread_func, NULL); pthread_join(thread1, NULL); pthread_join(thread2, NULL); printf("global_variable value: %d\n", global_variable); return 0; }

In this example, two threads increment the value of a global variable. However, because the execution order of the threads is not guaranteed, the final value of the global variable is unpredictable and can vary between different executions of the program.

Double Fetch example:

#include <stdio.h> void do_something(int* buffer, int length){ if (length < 1){ return; } int value = buffer[0]; if (value == 0){ return; } int* ptr = (int*)value; int data = *ptr; if (data == 42){ printf("Success!\n"); } } int main(){ int buffer[2] = {0}; int* ptr = (int*)buffer; *ptr = (int)&buffer[1]; buffer[1] = 42; do_something(buffer, 1); do_something(buffer, 1); return 0; }

In this example, the do_something() function takes a buffer of integers and fetches the first integer, treating it as a memory address. If the value at that memory address is 42, the function prints "Success!". However, the do_something() function is called twice with the same buffer, which allows an attacker to modify the buffer between the two calls, leading to a double fetch vulnerability.

Time of Check, Time of Use (TOCTOU) example:

c
#include <stdio.h> #include <fcntl.h> #include <unistd.h> void do_something(const char* filename, int uid){ int fd = open(filename, O_RDONLY); if (fd < 0){ perror("open"); return; } struct stat st; if (fstat(fd, &st) < 0){ perror("fstat"); close(fd); return; } if (st.st_uid != uid){ printf("Access denied\n");

    close(fd);     return;     } char buffer[1024]; ssize_t n = read(fd, buffer, sizeof(buffer)); close(fd); if (n < 0){ perror("read"); return; } printf("File contents: %.*s\n", (int)n, buffer); }

int main(){ const char* filename = "/etc/passwd"; int uid = getuid();

 

do_something(filename, uid); return 0;

}

In this example, the `do_something()` function takes a filename and a user ID as arguments and tries to read the contents of the file. However, before reading the file, the function checks if the user ID matches the owner of the file. This is intended to prevent unauthorized access to the file. However, this check is vulnerable to a Time of Check, Time of Use (TOCTOU) attack because the state of the file (including its owner) can change between the time it is checked and the time it is read. An attacker can exploit this vulnerability by replacing the file with a symbolic link to a file owned by a different user.

The following is an example of how this can be done:

$ ln -s /etc/shadow /tmp/passwd $ chown root /tmp/passwd $ ./example Access denied

In this example, the attacker creates a symbolic link from `/tmp/passwd` to `/etc/shadow`, which is owned by the root user. The attacker then changes the owner of the link to root. When the `do_something()` function is called with the `/tmp/passwd` filename, the check for the owner of the file returns true because the link is owned by root. However, when the function tries to read the contents of the file, it actually reads the contents of `/etc/shadow`, which contains sensitive information that should only be accessible by the root user.

No comments:

Post a Comment

A Guide to Multi-Level Pointer Analysis

  A Comprehensive Guide to Multi-Level Pointer Analysis   A regular pointer points to only one address, but when it's accompanied by a l...