my low-level programming blog
Αυτό είναι το υποχρεωτικό μέρος της άσκησης riddle
που
είναι η πρώτη στο εργαστήριο λειτουργικών συστημάτων της ΣΗΜΜΥ
ΕΜΠ.
Μία πολύ ενδιαφέρουσα reverse-engineering άσκηση στο user space.
Μπορείτε να κατεβάσετε το εκτελέσιμο (linux x86_64) για να παίξετε μόνοι
σας από εδώ
Εκτελούμε το πρόγραμμα και παρατηρούμε την έξοδο. Γνωρίζουμε από την
εκφώνηση πως χρησιμοποιεί κλήσεις συστήματος, οπότε με το
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
Η δεύτερη πρόκληση φαίνεται να είναι προφανής, καθώς αναφέρει το
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 επιτυγχάνει.
Πριν προχωρήσουμε με τις επόμενες, ας επανέλθουμε στην πρώτη πρόκληση.
Το 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
επιτυγχάνει.
Η τρίτη πρόκληση μας λέει να χρησιμοποιήσουμε 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
Το πρόγραμμα δεν βρίσκει την αντανάκλαση του.
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 επιτυγχάνει.
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);
(fd, 99);
dup2
("./riddle", NULL, NULL);
execve(fd);
closereturn 0;
}
Γράφουμε ένα απλό πρόγραμμα σε C και καλούμε το πρόγραμμα αφού
δημιουργήσουμε το fd
. Η πρόκληση επιτυγχάνει.
Η 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
και η πρόκληση
επιτυγχάνει.
Ας επιστρέψουμε στην 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);
(r, 33);
dup2(w, 34);
dup2
("./riddle", NULL, NULL);
execve(r);
close(w);
closereturn 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
. Ας τα ορίσουμε κι αυτά.
...
(r, 53);
dup2(w, 54);
dup2...
το πρόγραμμα προχωράει, αλλά και πάλι αποτυγχάνει
[2758] PING!
[2758] PONG!
[2758] PING!
[2759] PONG!
[2759] PING!
FAIL
Και πάλι, κάτι φαίνεται να μην έχει δουλέψει με τον σωστό τρόπο. Ας δοκιμάσουμε να στήσουμε pipes ώστε να διευκολύνουμε την επικοινωνία μεταξύ των διεργασιών
#include <unistd.h>
int main()
{
int p1[2], p2[2];
(p1);
pipe(p1[0], 33);
dup2(p1[1], 34);
dup2
(p2);
pipe(p2[0], 53);
dup2(p2[1], 54);
dup2
("./riddle", NULL, NULL);
execvereturn 0;
}
κι επιτέλους δουλεύει.
Το πρόγραμμα προσπαθεί να ανοίξει το αρχείο 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
και η πρόκληση επιτυγχάνει.
Με 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
το οποίο μας στέλνει μια απλή
πρόσθεση. Όταν απαντήσουμε σωστά η πρόκληση πετυχαίνει.
Με 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
ανοίγει το secret_number
- ας το δημιουργήσουμε
touch secret_number
σβήνει το μοναδικό link που υπάρχει για το
secret_number
(πριν προλάβουμε να το διαβάσουμε), δηλαδή το
όνομα του αρχείου από το σύστημα αρχείων
γράφει αριθμό που σκέφτεται
κλείνει
Αυτό που πρέπει να κάνουμε λοιπόν, είναι να δημιουργήσουμε ένα δεύτερο hard link ώστε όταν η διεργασία διαγράψει το πρώτο, εμείς να συνεχίσουμε να έχουμε πρόσβαση στο περιεχόμενο του αρχείου και δηλαδή την απάντηση.
link secret_number distraction
Τρέχουμε το πρόγραμμα και κοιτάζουμε το αρχείο
cat distraction
The number I am thinking of right now is [uppercase matters]: 02F7607B
.
Προσπαθούμε με τον ίδιο τρόπο, αλλά εδώ υπάρχει μια διαφορά. Η
διεργασία καλεί την fstat
, κατά πάσα πιθανότητα για να δει
αν υπάρχουν περισσότερα από τα επιτρεπτά hard links. Ο μοναδικός τρόπος
να προχωρήσουμε είναι να διατηρήσουμε το αρχείο ανοίγοντας το παράλληλα
από μία διαφορετική διεργασία.
Υπάρχει ένα πρόγραμμα που κάνει ακριβώς αυτό,
tail -f secret_number
, και τρέχουμε στο άλλο tab το
riddle
. Βλέπουμε
The number I am thinking of right now is [uppercase matters]: 884BD5FB
και η πρόκληση επιτυγχάνει.
Η συγκεκριμένη πρόκληση φαίνεται πιο περίπλοκη, οπότε θα ήθελα ιδανικά να ασχοληθώ με την 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("say the line: ")
input_string = input_string.split("'")[1]
char_to_insert print(f"parsed char: {char_to_insert}")
= "/tmp/"
tmp_dir = [f for f in os.listdir(tmp_dir) if f.startswith("riddle-")]
files
if len(files) != 1:
raise Exception("there should be exactly one file in the tmp directory")
= os.path.join(tmp_dir, files[0])
file_path
with open(file_path, 'r+b') as file:
file.seek(111)
file.write(char_to_insert.encode())
Τρέχουμε το riddle
και μεταφέρουμε τις πληροφορίες στο
script που περιμένει. Η πρόκληση πετυχαίνει έτσι.
Με 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
και η πρόκληση
πετυχαίνει
Δεν χρειάζεται καν 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.