naked_code

my low-level programming blog

home

riddle

Αυτό είναι το υποχρεωτικό μέρος της άσκησης riddle που είναι η πρώτη στο εργαστήριο λειτουργικών συστημάτων της ΣΗΜΜΥ ΕΜΠ.
Μία πολύ ενδιαφέρουσα reverse-engineering άσκηση στο user space.
Μπορείτε να κατεβάσετε το εκτελέσιμο (linux x86_64) για να παίξετε μόνοι σας από εδώ


Υποχρεωτικά

Challenge 00

Εκτελούμε το πρόγραμμα και παρατηρούμε την έξοδο. Γνωρίζουμε από την εκφώνηση πως χρησιμοποιεί κλήσεις συστήματος, οπότε με το strace τις διαβάζουμε.

alex@debian:~/riddle-20240930-0$ ./riddle

Challenge   0: 'Hello there'
Hint:          'Processes run system calls'.
FAIL

Next challenge locked. Complete more challenges.

Το αποτέλεσμα του strace δείχνει πως το πρόγραμμα προσπαθεί να ανοίξει το αρχείο .hello_there ωστόσο αποτυγχάνει επειδή δεν υπάρχει

openat(AT_FDCWD, ".hello_there", O_RDONLY) = -1 ENOENT (No such file or directory)

Δημιουργούμε το .hello_there με την touch .hello_there και το Challenge 0 επιτυγχάνει.

Ξεκλειδώνουν δύο προκλήσεις. Η μία γράφει:

Challenge   1: 'Gatekeeper'
Hint:          'Stand guard, let noone pass'.
... I found the doors unlocked. FAIL

και η άλλη:

Challenge   2: 'A time to kill'
Hint:          'Stuck in the dark, help me move on'.
You were eaten by a grue. FAIL

Challenge 02

Η δεύτερη πρόκληση φαίνεται να είναι προφανής, καθώς αναφέρει το kill. Το πρόγραμμα παγώνει και μετά μας τρώει ένα Grue.

Ας προσπαθήσουμε να το σκοτώσουμε εμείς πρώτοι. Κοιτάμε το strace

rt_sigaction(SIGALRM, {sa_handler=0x563eb82be350, sa_mask=[ALRM], sa_flags=SA_RESTORER|SA_RESTART, sa_restorer=0x7fc7155ae050}, {sa_handler=SIG_DFL, sa_mask=[], sa_flags=0}, 8) = 0
rt_sigaction(SIGCONT, {sa_handler=0x563eb82be350, sa_mask=[CONT], sa_flags=SA_RESTORER|SA_RESTART, sa_restorer=0x7fc7155ae050}, {sa_handler=SIG_DFL, sa_mask=[], sa_flags=0}, 8) = 0
alarm(10)                               = 0
pause()                                 = ? ERESTARTNOHAND (To be restarted if no handler)
--- SIGALRM {si_signo=SIGALRM, si_code=SI_KERNEL} ---

το πρόγραμμα προετοιμάζει δύο σήματα, το SIGALRM και το SIGCONT. Περιμένει κάποιο σήμα για 10 δευτερόλεπτα (είτε κλείνει την διεργασία είτε κάποιο άλλο σήμα, δηλαδή συνήθως Ctrl-C ή Ctrl-Z) και μετά κλείνει (η προεπιλεγμένη λειτουργία του SIGALRM).

Δοκιμάζουμε να κλείσουμε την διεργασία με Ctrl-C και να ξανατρέξουμε το πρόγραμμα, αλλά δεν φαίνεται να έχει προχωρήσει. Συνεπώς περιμένει για κάτι άλλο. Ας σταματήσουμε την διεργασία με Ctrl-Z ενώ περιμένει κι ας την επαναφέρουμε με fg (μπορούμε επίσης και να στείλουμε ένα σήμα kill -SIGCONT PID).

Το Challenge 2 επιτυγχάνει.

Challenge 01

Πριν προχωρήσουμε με τις επόμενες, ας επανέλθουμε στην πρώτη πρόκληση.

Το strace βγάζει αυτό το αποτέλεσμα:

openat(AT_FDCWD, ".hello_there", O_WRONLY) = 4

Ενδιαφέρον, προσπαθεί να ανοίξει το .hello_there σε write only mode και πετυχαίνει. Πετυχαίνει φυσικά επειδή δεν το έχουμε περιορίσει.

Stand guard, let noone pass

Αν αφαιρέσουμε τα δικαιώματα επεξεργασίας του αρχείου με chmod a-w .hello_there, βλέπουμε πως το Challenge 1 επιτυγχάνει.

Challenge 03

Η τρίτη πρόκληση μας λέει να χρησιμοποιήσουμε ltrace. Όταν το κάνουμε, βλέπουμε πως τρέχει την εντολή getenv("ANSWER"). Ψάχνει για κάποια μεταβλητή περιβάλλοντος ANSWER, η οποία προφανώς δεν υπάρχει και αποτυγχάνει.

Το what is the answer to life the universe and everything? είναι αναφορά στο βιβλίο The Hitchhiker’s Guide to the Galaxy και η «απάντηση» είναι 42.

export ANSWER=42

Challenge 04

Το πρόγραμμα δεν βρίσκει την αντανάκλαση του.

openat(AT_FDCWD, "magic_mirror", O_RDWR) = -1 ENOENT (No such file or directory)

Αρχικά δοκιμάζω να το αντιγράψω κυριολεκτικά cp riddle magic_mirror, ωστόσο δεν λειτουργεί. Παρατηρώ πως strace δείχνει ότι προσπαθεί να γράψει και να διαβάσει ένα byte από το αρχείο. Συνεπώς, μάλλον δεν θέλει κυριολεκτικά το ομοίωμά του, αλλά χρειάζεται ένα αρχείο που θα γράφει και θα διαβάζει τον ίδιο χαρακτήρα, στην ίδια θέση που τον έγραψε. Με βάση τις οδηγίες στο εργαστήριο, το μυαλό μου πάει αμέσως στα named pipes.

mkfifo magic_mirror και το challenge επιτυγχάνει.

Challenge 05

fd, άρα σκεφτόμαστε file descriptor flags. Αυτά είναι τοπικά για κάθε διεργασία του λειτουργικού, οπότε μάλλον πρέπει η διεργασία μας να βρει ένα file descriptor flag με fd=99. Συμβουλευόμαστε τo dup(2) - Linux manual page και βλέπουμε ότι για να μπορούμε να ορίσουμε το καινούριο fd, πρέπει να χρησιμοποιήσουμε την dup2() και μέσα από αυτό το process να καλέσουμε το riddle, ώστε να μπορεί να βρεί το fd που μόλις ορίσαμε.

#include <unistd.h>
#include <fcntl.h>

int main() {
    int fd = open(".hello_there", O_RDONLY);
    dup2(fd, 99);

    execve("./riddle", NULL, NULL);
    close(fd);
    return 0;
}

Γράφουμε ένα απλό πρόγραμμα σε C και καλούμε το πρόγραμμα αφού δημιουργήσουμε το fd. Η πρόκληση επιτυγχάνει.

Challenge 07

Η 7η πρόκληση είναι πιο εύκολη, οπότε θα προχωρήσουμε με αυτήν. Με strace βλέπουμε πως μια lstat αποτυγχάνει διότι δεν υπάρχει το αρχείο .hey_there που προσπαθεί να προσπελάσει.

Δημιουργούμε το αρχείο και ξανατρέχουμε το riddle και τώρα βλέπουμε μια διαφορετική έξοδο Oops. 130825 != 130835.

Η lstat διαβάζει πληροφορίες για ορισμένα αρχεία. Φανταζόμαστε ότι το 130825 και το 130835 είναι πληροφορίες για τα δύο αρχεία που προσπαθεί να προσπελάσει (.hello_there, .hey_there)

Τρέχοντας την stat .hello_there και stat .hey_there, βλέπουμε πως όντως είναι το μοναδικό αναγνωριστικό των δύο αρχείων. Άρα τα inodes πρέπει να είναι ίδια για να πετύχει η πρόκληση. Αυτό επιτυγχάνεται όταν είναι hard links στο ίδιο αρχείο.

Οπότε link .hey_there .hello_there και η πρόκληση επιτυγχάνει.

Challenge 06

Ας επιστρέψουμε στην 6η πρόκληση πριν προχωρήσουμε.

Στο strace βλέπουμε τα εξής

clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7fe60f185a10) = 2386
clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD[2386] PING!
, child_tidptr=0x7fe60f185a10) = 2387
wait4(-1, [{WIFEXITED(s) && WEXITSTATUS(s) == 1}], 0, NULL) = 2386
--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=2386, si_uid=1000, si_status=1, si_utime=0, si_stime=0} ---
wait4(-1, [{WIFEXITED(s) && WEXITSTATUS(s) == 1}], 0, NULL) = 2387
--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=2387, si_uid=1000, si_status=1, si_utime=0, si_stime=0} ---

Το riddle δημιουργεί child processes κι από το ping-pong καταλαβαίνουμε ότι θα πρέπει να επικοινωνούν μεταξύ τους, η μία διεργασία να στέλνει ping κι η άλλη pong.

Για να ελέγξουμε τι γίνεται στις υποδιεργασίες, τρέχουμε strace -f ./riddle και βλέπουμε πως όντως οι διεργασίες προσπαθούν να επικοινωνήσουν μεταξύ τους γράφοντας και διαβάζοντας από συγκεκριμένα fd

[pid 2518] read(33, 0x7ffd53daf064, 4) = -1 EBADF (Bad file descriptor)

[pid 2517] write(34, "\0\0\0\0", 4 = -1 EBADF (Bad file descriptor)

η 2518 διαβάζει από το 33

η 2517 γράφει στο 34.

Ας ξαναδημιουργήσουμε λοιπόν τα κατάλληλα fd:

#include <unistd.h>
#include <fcntl.h>

int main()
{
        int r = open(".hello_there", O_RDONLY);
        int w = open(".hey_there", O_WRONLY);
        dup2(r, 33);
        dup2(w, 34);

        execve("./riddle", NULL, NULL);
        close(r);
        close(w);
        return 0;

Αποτυγχάνει και πάλι, κοιτώντας το strace -f βλέπουμε ότι πλέον

[pid 2723] read(53, 0x7ffd70be2c94, 4) = -1 EBADF (Bad file descriptor)

[pid 2724] write(54, "\1\0\0\0" = -1 EBADF (Bad file descriptor)

η μία αποτυγχάνει να διαβάσει από το 53 κι η άλλη αποτυγχάνει να γράψει στο 54. Ας τα ορίσουμε κι αυτά.

...
    dup2(r, 53);
    dup2(w, 54);
...

το πρόγραμμα προχωράει, αλλά και πάλι αποτυγχάνει

[2758] PING!
[2758] PONG!
[2758] PING!
[2759] PONG!
[2759] PING!
FAIL

Και πάλι, κάτι φαίνεται να μην έχει δουλέψει με τον σωστό τρόπο. Ας δοκιμάσουμε να στήσουμε pipes ώστε να διευκολύνουμε την επικοινωνία μεταξύ των διεργασιών

#include <unistd.h>

int main()
{
        int p1[2], p2[2];

        pipe(p1);
        dup2(p1[0], 33);
        dup2(p1[1], 34);

        pipe(p2);
        dup2(p2[0], 53);
        dup2(p2[1], 54);

        execve("./riddle", NULL, NULL);
        return 0;
}

κι επιτέλους δουλεύει.

Challenge 08

Το πρόγραμμα προσπαθεί να ανοίξει το αρχείο bf00, το δημιουργούμε.

Τρέχουμε το strace ως συνήθως και βλέπουμε (ενδεικτικά) ότι

openat(AT_FDCWD, "bf00", O_RDONLY)      = 4
lseek(4, 1073741824, SEEK_SET)          = 1073741824
read(4, "", 16)                         = 0
write(1, "X", 1X)                        = 1
close(4)                                = 0

το πρόγραμμα προσπελάζει το bf00 στο 1073741824 byte (=1GB) και διαβάζει 16 bytes, τα οποία είναι κενά. Γράφει Χ στο stdout και κλείνει.

Διευρύνουμε το αρχείο με truncate -s 1G bf00 και γράφουμε 16 bytes στο τέλος του αρχείου echo -n "1234567890123456" >> bf00. Πετυχαίνει αλλά τώρα ψάχνει για το bf01.

Επειδή αυτό από ό,τι φαίνεται συνεχίζεται και μετά το bf01, μπορούμε είτε να δημιουργήσουμε όσα αρχεία όσα χρειάζεται - cp bf00 bf01 (μέχρι στιγμής πάνω από 3GB σε μέγεθος), ή μπορούμε να κάνουμε κάτι πιο έξυπνο και να φτιάξουμε links για κάθε αρχείο που ψάχνει με το αρχικό μας αρχείο- link bf00 bf01, εφόσον το footer δεν χρειάζεται να αλλάζει.

Φτάνουμε μέχρι το bf09 και η πρόκληση επιτυγχάνει.

Challenge 09

Με strace βλέπουμε ότι η διεργασία περιμένει ένα IP socket στην 49842.

connect(4, {sa_family=AF_INET, sin_port=htons(49842), sin_addr=inet_addr("127.0.0.1")}, 16) = -1 ECONNREFUSED (Connection refused)

Η σύνδεση αποτυγχάνει, όντως δεν υπάρχει κάτι που να ακούει σε αυτή την θύρα (netstat -tuln | grep 49842)

Μπορούμε να σετάρουμε έναν TCP server που θα ακούει σε αυτήν την θύρα με πολλούς τρόπους, χρησιμοποιώντας το socket library της Python, γράφοντας ένα απλό πρόγραμμα σε C με το arpa/inet.h ή ακόμη πιο απλά με το netcat -l 49842

Επιλέγουμε το τρίτο και απλούστερο, το τρέχουμε σε ένα δεύτερο τερματικό, τρέχουμε το riddle το οποίο μας στέλνει μια απλή πρόσθεση. Όταν απαντήσουμε σωστά η πρόκληση πετυχαίνει.

Challenge 10

Με strace έχουμε:

openat(AT_FDCWD, "secret_number", O_RDWR|O_CREAT|O_TRUNC, 0600) = 4
unlink("secret_number")                 = 0
write(4, "The number I am thinking of righ"..., 4096) = 4096
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_SHARED, 4, 0) = 0x7f5bc5beb000
close(4)                                = 0

Αυτό που πρέπει να κάνουμε λοιπόν, είναι να δημιουργήσουμε ένα δεύτερο hard link ώστε όταν η διεργασία διαγράψει το πρώτο, εμείς να συνεχίσουμε να έχουμε πρόσβαση στο περιεχόμενο του αρχείου και δηλαδή την απάντηση.

link secret_number distraction

Τρέχουμε το πρόγραμμα και κοιτάζουμε το αρχείο cat distraction

The number I am thinking of right now is [uppercase matters]: 02F7607B.

Challenge 11

Προσπαθούμε με τον ίδιο τρόπο, αλλά εδώ υπάρχει μια διαφορά. Η διεργασία καλεί την fstat, κατά πάσα πιθανότητα για να δει αν υπάρχουν περισσότερα από τα επιτρεπτά hard links. Ο μοναδικός τρόπος να προχωρήσουμε είναι να διατηρήσουμε το αρχείο ανοίγοντας το παράλληλα από μία διαφορετική διεργασία.

Υπάρχει ένα πρόγραμμα που κάνει ακριβώς αυτό, tail -f secret_number, και τρέχουμε στο άλλο tab το riddle. Βλέπουμε The number I am thinking of right now is [uppercase matters]: 884BD5FB και η πρόκληση επιτυγχάνει.

Challenge 12

Η συγκεκριμένη πρόκληση φαίνεται πιο περίπλοκη, οπότε θα ήθελα ιδανικά να ασχοληθώ με την 13, ωστόσο η 12 με βάζει να περιμένω κάποια δευτερόλεπτα μέχρι να φτάσω στην 13 και βαριέμαι.

openat(AT_FDCWD, "/tmp/riddle-7vtlTJ", O_RDWR|O_CREAT|O_EXCL, 0600) = 4
ftruncate(4, 4096)                      = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_SHARED, 4, 0) = 0x7fbf80d50000
write(2, "I want to find the char 'T' at 0"..., 46I want to find the char 'T' at 0x7fbf80d5006f
) = 46

Ανοίγει ένα αρχείο στο ./tmp/riddle-ABCDEF και απαιτεί να αλλάξουμε τον χαρακτήρα στην θέση μνήμης 0x7fbf80d5006f. Το αρχείο πέρασε στη μνήμη στην θέση 0x7fbf80d50000, οπότε εύκολα κάνοντας την αφαίρεση βρίσκουμε το μήκος του αρχείου να είναι 0x00000000006f ή 111 bytes.

Με βάση αυτά που λέει

Challenge  12: 'A delicate change'
Hint:          'Do only what is required, nothing more, nothing less'.

πρέπει να αλλάξουμε συγκεκριμένα αυτό και τίποτα άλλο, την ώρα που τρέχει το πρόγραμμα.

Άρα δεν γνωρίζουμε το όνομα του αρχείου και το byte που πρέπει να γράψουμε στην θέση 111.

Θέλουμε το πρόγραμμα να τρέχει μέσα σε ένα πρόγραμμα, το οποίο θα διαβάζει αυτόματα το byte που πρέπει να τοποθετήσουμε και το όνομα του αρχείου από την έξοδο STDOUT. Αυτό δεν είναι τόσο απλό, συνεπώς εφόσον η πρόκληση μας το επιτρέπει, θα χρησιμοποιήσουμε τον χρόνο που μας δίνει για να μεταφέρουμε εμείς τις πληροφορίες με το χέρι.

import os

input_string = input("say the line: ")
char_to_insert = input_string.split("'")[1]
print(f"parsed char: {char_to_insert}")

tmp_dir = "/tmp/"
files = [f for f in os.listdir(tmp_dir) if f.startswith("riddle-")]

if len(files) != 1:
    raise Exception("there should be exactly one file in the tmp directory")

file_path = os.path.join(tmp_dir, files[0])

with open(file_path, 'r+b') as file:
    file.seek(111)
    file.write(char_to_insert.encode())

Τρέχουμε το riddle και μεταφέρουμε τις πληροφορίες στο script που περιμένει. Η πρόκληση πετυχαίνει έτσι.

Challenge 13

Με strace βλέπουμε

openat(AT_FDCWD, ".hello_there", O_RDWR|O_CREAT, 0600) = 4

το πρόγραμμα προσπαθεί να ανοίξει και να γράψει στο .hello_there. Συνεπώς αλλάζουμε και πάλι τα δικαιώματά του, chmod a+w .hello_there.

openat(AT_FDCWD, ".hello_there", O_RDWR|O_CREAT, 0600) = 4
ftruncate(4, 32768)                     = 0
mmap(NULL, 32768, PROT_READ|PROT_WRITE, MAP_SHARED, 4, 0) = 0x7f3507a62000
ftruncate(4, 16384)                     = 0

Ανοίγει το αρχείο, το μεγαλώνει στα 32768 bytes, το αποθηκεύει στη μνήμη και μετά το σμικρύνει στα 16384 bytes. Άρα η μνήμη που μέχρι πρότινος αποδίδετο στο αρχείο, πλέον δεν δείχνει σε τίποτα.

Ας δοκιμάσουμε να ξαναμεγαλώσουμε το πρόγραμμα στο κανονικό του μέγεθος. Τρέχουμε το riddle και σε ένα καινούριο shell τρέχουμε truncate -s 32768 .hello_there

Δίνουμε ένα input στο riddle και η πρόκληση πετυχαίνει

Challenge 14

Δεν χρειάζεται καν strace για να καταλάβουμε τι συμβαίνει. Πρέπει να τρέξουμε το riddle και να έχει συγκεκριμένα το PID = 32767.

Τα linux συνήθως δίνουν PID από 1 έως 32768. Για κάθε καινούρια διεργασία, αποδίδουν το μικρότερο δυνατό PID. Συνεπώς, για να φτάσουμε στο επιθυμητό, πρέπει να δημιουργήσουμε όσες διεργασίες χρειάζονται μέχρι να έρθει η σειρά της 32767ης, στην οποία θα τρέξουμε το πρόγραμμα

Ίσως και να υπάρχει όμως κι άλλος τρόπος. Το τελευταίο PID αποθηκεύεται στο /proc/sys/kernel/ns_last_pid. Αν το αλλάξουμε, ίσως ο kernel αποδώσει το επόμενο από το τελευταίο και επιτύχει το πρόγραμμα.

echo 32765 > /proc/sys/kernel/ns_last_pid

Μετά από κάποιες απόπειρες να τρέξουμε το riddle, παίρνουμε το επιθυμητό αποτέλεσμα και η πρόκληση πετυχαίνει.

Συνολικά αποτελέσματα

./riddle:

Challenge   0: 'Hello there'
Welcome back challenger. You may pass.

Challenge   1: 'Gatekeeper'
Welcome back challenger. You may pass.

Challenge   2: 'A time to kill'
Welcome back challenger. You may pass.

Challenge   3: 'what is the answer to life the universe and everything?'
Welcome back challenger. You may pass.

Challenge   4: 'First-in, First-out'
Welcome back challenger. You may pass.

Challenge   5: 'my favourite fd is 99'
Welcome back challenger. You may pass.

Challenge   6: 'ping pong'
Welcome back challenger. You may pass.

Challenge   7: 'What's in a name?'
Welcome back challenger. You may pass.

Challenge   8: 'Big Data'
Welcome back challenger. You may pass.

Challenge   9: 'Connect'
Welcome back challenger. You may pass.

Challenge  10: 'ESP'
Welcome back challenger. You may pass.

Challenge  11: 'ESP-2'
Welcome back challenger. You may pass.

Challenge  12: 'A delicate change'
Welcome back challenger. You may pass.

Challenge  13: 'Bus error'
Welcome back challenger. You may pass.

Challenge  14: 'Are you the One?'
Welcome back challenger. You may pass.