Ctrl

Thoughts of a simple, single-threaded human constantly trying to find the meaning of life live, love and learn in the modern world.

The Riddle Attacked (Tier 1)

I have a "Systems" interview coming up.

The interview description states: "Linux internals and troubleshooting".

And continues: "Everything from the kernel to the user space is fair game" :)

So, I wanted to really fresh up my UNIX skills.

The Basics

For starters, I re-watched the following amazing YouTube videos made by Brian Will:

The Riddle

Next, I remembered an interesting exercise from the "Operating Systems Lab" course at ECE NTUA.

Luckily, that exercise is now available online, for everyone to play.

I will try to beat this game -again-, this time recording and explaining every step.

I believe this should be both a good hands-on practice, as well as an opportunity to express my thoughts in English.

List of Challenges

Challenge 0

The game is loaded and I can see an interactive shell prompt:

riddle@host01:~$ whoami
riddle

To solve the riddle, I must run the riddle executable:

riddle@host01:~$ ./riddle

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

Next challenge locked. Complete more challenges.

I must therefore use the strace utility to inspect the system calls performed by the riddle executable.

The output, near the end, is:

riddle@host01:~$ strace ./riddle
.
.
.
write(2, "Hint:          'Processes run sy"..., 45Hint:          'Processes run system calls'.
) = 45
open(".hello_there", O_RDONLY)          = -1 ENOENT (No such file or directory)
write(2, "\33[31mFAIL\33[0m\n", 14FAIL
)     = 14

The riddle tried to open(2) a file called .hello_there and failed with ENOENT because such file does not exist.

Let me create the file and run the riddle again:

riddle@host01:~$ touch .hello_there
riddle@host01:~$ ./riddle

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

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

Next challenge locked. Complete more challenges.
riddle@host01:~$

Voila! The riddle found the .hello_there file and moved on.

Challenge 0 passed!

riddle@host01:~$ cat riddle.savegame
56BA6BB80350DF234409301C755F155931C3104C

Challenge 1

Let's tackle the next challenge, by running the riddle executable:

riddle@host01:~$ ./riddle

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

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

Next challenge locked. Complete more challenges.

The hint reads "I found the doors unlocked". Should it be locked?

Let me use strace again to inspect what system calls are performed under the hood:

riddle@host01:~$ strace ./riddle
.
.
.
write(2, "\nChallenge   1: 'Gatekeeper'\n", 29
Challenge   1: 'Gatekeeper'
) = 29
write(2, "Hint:          'Stand guard, let"..., 46Hint:          'Stand guard, let noone pass'.
) = 46
open(".hello_there", O_WRONLY)          = 4
close(4)                                = 0
write(2, "\33[31m... I found the doors unloc"..., 46... I found the doors unlocked. FAIL
) = 46

There seems to be no related system call that has failed.

But maybe this is the problem, also according to the hint message.

In Challenge 0 the open(2) system call had the O_RDONLY parameter, but this time it has the O_WRONLY parameter.

Let me try forcing the open(2) syscall to fail, by changing the .hello_there file permissions, and removing the write permissions:

riddle@host01:~$ chmod 444 .hello_there
riddle@host01:~$ ./riddle

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

Challenge   1: 'Gatekeeper'
Hint:          'Stand guard, let noone pass'.
SUCCESS

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

Challenge   3: 'what is the answer to life the universe and everything?'
Hint:          'ltrace'.
FAIL

Next challenge locked. Complete more challenges.
riddle@host01:~$

Voila! The riddle could not open(2) the .hello_there file with O_WRONLY and moved on.

Challenge 1 passed!

riddle@host01:~$ cat riddle.savegame
56BA6BB80350DF234409301C755F155931C3104C
D5AB23E8FA85E1B93CFB9073B635DAFA39DC1520

Challenge 2

Let's tackle the next challenge, by running the riddle executable:

riddle@host01:~$ ./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'
Hint:          'Stuck in the dark, help me move on'.
You were eaten by a grue. FAIL

Challenge   3: 'what is the answer to life the universe and everything?'
Hint:          'ltrace'.
FAIL

Next challenge locked. Complete more challenges.

What is interesting here, is that the following failure message does not appear immediately, rather after 10 seconds:

You were eaten by a grue. FAIL

Also the challenge title is "A time to kill" so it must be something related to timing.

Once again, let's use strace to inspect what the riddle is doing:

riddle@host01:~$ strace ./riddle
.
.
.
write(2, "\nChallenge   2: 'A time to kill'"..., 33
Challenge   2: 'A time to kill'
) = 33
write(2, "Hint:          'Stuck in the dar"..., 53Hint:          'Stuck in the dark, help me move on'.
) = 53
rt_sigaction(SIGALRM, {0x5585fcab5d40, [ALRM], SA_RESTORER|SA_RESTART, 0x7fa79b9034b0}, {SIG_DFL, [], 0}, 8) = 0
rt_sigaction(SIGCONT, {0x5585fcab5d40, [CONT], SA_RESTORER|SA_RESTART, 0x7fa79b9034b0}, {SIG_DFL, [], 0}, 8) = 0
alarm(10)                               = 0
pause()                                 = ? ERESTARTNOHAND (To be restarted if no handler)
--- SIGALRM {si_signo=SIGALRM, si_code=SI_KERNEL} ---
rt_sigreturn({mask=[]})                 = -1 EINTR (Interrupted system call)
write(2, "\33[31mYou were eaten by a grue. F"..., 40You were eaten by a grue. FAIL
) = 40

Hmmm...

Right after printing the challenge title and hint message, I see two rt_sigaction lines.

The sigaction(2) and rt_sigaction(2) system calls are used to change the action taken by a process on receipt of a specific signal.

Next, the riddle uses the alarm(2) system call to arrange for a SIGALRM signal to be delivered to it in 10 seconds.

Finally, the riddle calls pause(2) to sleep until a signal is delivered that either terminates the process or causes the invocation of a signal-catching function.

Then after 10 seconds, the SIGALRM is received, so the pause(2) system call returns and probably, the SIGALRM signal handler is called as setup earlier.

At this point, I am not exactly sure why the challenge fails, but my guess is that it fails because the SIGALRM handler does so.

So, instead, let me invoke the SIGCONT signal handler, to see what will happen.

To do so, I will send the SIGCONT signal to the riddle executable:

iddle@host01:~$ ./riddle & sleep 5; pkill --signal SIGCONT riddle
[1] 20254

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'
Hint:          'Stuck in the dark, help me move on'.
SUCCESS

Challenge   3: 'what is the answer to life the universe and everything?'
Hint:          'ltrace'.
FAIL

Challenge   4: 'First-in, First-out'
Hint:          'Mirror, mirror on the wall, who in this land is fairest of
all?'.
I cannot see my reflection. FAIL

Next challenge locked. Complete more challenges.
[1]+  Exit 1                  ./riddle

Voila! The riddle received the SIGCONT signal and followed a different code path, succeeded, and moved on.

Challenge 2 passed!

riddle@host01:~$ cat riddle.savegame
56BA6BB80350DF234409301C755F155931C3104C
D5AB23E8FA85E1B93CFB9073B635DAFA39DC1520
12BABA77EB6CB7DA6921CAEAEBC8BFB15F15E1AD

Challenge 3

Let's tackle the next challenge, by running the riddle executable:

riddle@host01:~$ ./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?'
Hint:          'ltrace'.
FAIL

Challenge   4: 'First-in, First-out'
Hint:          'Mirror, mirror on the wall, who in this land is fairest of
all?'.
I cannot see my reflection. FAIL

Next challenge locked. Complete more challenges.

The hint for the next challenge reads "ltrace", so let me run the riddle executable under ltrace:

riddle@host01:~$ ltrace ./riddle
.
.
.
fwrite("\033[36mWelcome back challenger. Yo"..., 1, 48, 0x7f6a01fdf540Welcome back challenger. You may pass.
)                                    = 48
fprintf(0x7f6a01fdf540, "\nChallenge %3d: '%s'\n", 3, "what is the answer to life the u"...
Challenge   3: 'what is the answer to life the universe and everything?'
)               = 74
memcmp(0x7f6a0240c07b, 0x555bc18ba7a0, 40, 20)                                                             = 0xffffffcc
fprintf(0x7f6a01fdf540, "Hint:          '%s'.\n", "ltrace"Hint:          'ltrace'.
)                                                = 25
getenv("ANSWER")                                                                                           = nil
fprintf(0x7f6a01fdf540, "\033[31m%s\033[0m\n", "FAIL"FAIL
)                                                     = 14

So the riddle executable tries to query the value of a ANSWER environment variable, but it gets nil since no such env var exists.

Let me run the riddle executable with modified environment:

riddle@host01:~$ ANSWER=42 ./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?'
Hint:          'ltrace'.
SUCCESS

Challenge   4: 'First-in, First-out'
Hint:          'Mirror, mirror on the wall, who in this land is fairest of all?'.
I cannot see my reflection. FAIL

Challenge   5: 'my favourite fd is 99'
Hint:          'when I bang my head against the wall it goes: dup! dup! dup!'.
FAIL

Next challenge locked. Complete more challenges.

Voila! The riddle found the ANSWER environment variable, set with the correct value 42, succeeded, and moved on.

Challenge 3 passed!

riddle@host01:~$ cat riddle.savegame
56BA6BB80350DF234409301C755F155931C3104C
D5AB23E8FA85E1B93CFB9073B635DAFA39DC1520
12BABA77EB6CB7DA6921CAEAEBC8BFB15F15E1AD
4B95AA1F5D6F3980F888F0B3FD84070A356AD8FA

Challenge 4

Let's tackle the next challenge, by running the riddle executable:

riddle@host01:~$ ./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'
Hint:          'Mirror, mirror on the wall, who in this land is fairest of
all?'.
I cannot see my reflection. FAIL

Challenge   5: 'my favourite fd is 99'
Hint:          'when I bang my head against the wall it goes: dup! dup! dup!'.
FAIL

Next challenge locked. Complete more challenges.

Although the hint isn't helping, let me try running the riddle executable under strace to see what is happening under the hood:

riddle@host01:~$ strace ./riddle
.
.
.
write(2, "\nChallenge   4: 'First-in, First"..., 38
Challenge   4: 'First-in, First-out'
) = 38
write(2, "Hint:          'Mirror, mirror o"..., 82Hint:          'Mirror, mirror on the wall, who in this land is fairest of all?'.
) = 82
open("magic_mirror", O_RDWR)            = -1 ENOENT (No such file or directory)
write(2, "\33[31mI cannot see my reflection."..., 42I cannot see my reflection. FAIL
) = 42

So, the riddle executable tried to open the magic_mirror file and failed. Let me create it and re-run the riddle executable.

Oops. Same error:

riddle@host01:~$ strace ./riddle
.
.
.
write(2, "\nChallenge   4: 'First-in, First"..., 38
Challenge   4: 'First-in, First-out'
) = 38
write(2, "Hint:          'Mirror, mirror o"..., 82Hint:          'Mirror, mirror on the wall, who in this land is fairest of all?'.
) = 82
open("magic_mirror", O_RDWR)            = 4
write(4, "U", 1)                        = 1
read(4, "", 1)                          = 0
write(2, "\33[31mI cannot see my reflection."..., 42I cannot see my reflection. FAIL
) = 42

Ok, I am going somewhere.

The open(2) system call on the magic_mirror file succeded and returned the file descriptor fd=4.

Then the riddle executable wrote 1 byte into the magic_mirror file: the byte U.

Then the riddle executable tried to read 1 byte, but failed.

The magic_mirror file was initially empty. The fd=4 was moved one byte forward after writing the U byte, so it was at the EOF.

OK, so my guess is that the riddle executable tries to write a random byte into the magic_mirror file and then read back the exact same byte.

In any regular file, a read after a write will always read the "next" byte, not the same as the one just written.

But, in a FIFO pipe, a read after a write would always read the last byte that was just written.

Therefore magic_mirror must be a FIFO named pipe, not a regular file:

riddle@host01:~$ rm magic_mirror
riddle@host01:~$ mkfifo magic_mirror
riddle@host01:~$ strace ./riddle
.
.
.
write(2, "\nChallenge   4: 'First-in, First"..., 38
Challenge   4: 'First-in, First-out'
) = 38
write(2, "Hint:          'Mirror, mirror o"..., 82Hint:          'Mirror, mirror on the wall, who in this land is fairest of all?'.
) = 82
open("magic_mirror", O_RDWR)            = 4
write(4, "W", 1)                        = 1
read(4, "W", 1)                         = 1
write(4, "D", 1)                        = 1
read(4, "D", 1)                         = 1
write(4, "Y", 1)                        = 1
read(4, "Y", 1)                         = 1
write(4, "O", 1)                        = 1
read(4, "O", 1)                         = 1
write(4, "U", 1)                        = 1
read(4, "U", 1)                         = 1
write(4, "L", 1)                        = 1
read(4, "L", 1)                         = 1
write(4, "U", 1)                        = 1
read(4, "U", 1)                         = 1
write(4, "B", 1)                        = 1
read(4, "B", 1)                         = 1
write(4, "L", 1)                        = 1
read(4, "L", 1)                         = 1
write(4, "B", 1)                        = 1
read(4, "B", 1)                         = 1
close(4)                                = 0
write(2, "\33[32mSUCCESS\33[0m\n", 17SUCCESS
)  = 17

Voila! The riddle executable wrote and then immediately read the same byte from the magic_mirror fifo, not once but ten times, succeeded, and moved on.

Challenge 4 passed!

riddle@host01:~$ cat riddle.savegame
56BA6BB80350DF234409301C755F155931C3104C
D5AB23E8FA85E1B93CFB9073B635DAFA39DC1520
12BABA77EB6CB7DA6921CAEAEBC8BFB15F15E1AD
4B95AA1F5D6F3980F888F0B3FD84070A356AD8FA
C3743AE3170A6BF66500369F3FCE942901A25363

Challenge 5

Let's tackle the next challenge, by running the riddle executable:

riddle@host01:~$ ./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'
Hint:          'when I bang my head against the wall it goes: dup! dup! dup!'.
FAIL

Challenge   6: 'ping pong'
Hint:          'help us play!'.
[2175] PING!
FAIL

Next challenge locked. Complete more challenges.

Let me run strace:

riddle@host01:~$ strace ./riddle
.
.
.
write(2, "\nChallenge   5: 'my favourite fd"..., 40
Challenge   5: 'my favourite fd is 99'
) = 40
write(2, "Hint:          'when I bang my h"..., 79Hint:          'when I bang my head against the wall it goes: dup! dup! dup!'.
) = 79
fcntl(99, F_GETFD)                      = -1 EBADF (Bad file descriptor)
write(2, "\33[31mFAIL\33[0m\n", 14FAIL
)     = 14

The riddle executable calls the fcntl(2) system call with the F_GETFD parameter to query the file descriptor flags on the file descriptor fd=99.

But no such file descriptor exists for the riddle process.

I can use the dup2(2) system call to duplicate an existing file descriptor and create the fd=99. The STDOUT file descriptor fd=1 is an example of an existing file descriptor.

Unfortunately, I don't know any trick for calling the dup2(2) system call from "outside" the process, so I must create the duplicate the file descriptor and then "manually create the riddle process" myself:

riddle@host01:~$ cat dup2.py
import os

os.dup2(1, 99)
os.execv("./riddle", ["riddle"])
riddle@host01:~$ python dup2.py

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'
Hint:          'when I bang my head against the wall it goes: dup! dup! dup!'.
SUCCESS

Challenge   6: 'ping pong'
Hint:          'help us play!'.
[19896] PING!
FAIL

Challenge   7: 'What's in a name?'
Hint:          'A rose, by any other name...'.
FAIL

Next challenge locked. Complete more challenges.

Voila! The riddle process, before running the riddle executrable, duplicated the fd=1 to fd=99, and so succeeded, and moved on.

Challenge 5 passed!

riddle@host01:~$ cat riddle.savegame
56BA6BB80350DF234409301C755F155931C3104C
D5AB23E8FA85E1B93CFB9073B635DAFA39DC1520
12BABA77EB6CB7DA6921CAEAEBC8BFB15F15E1AD
4B95AA1F5D6F3980F888F0B3FD84070A356AD8FA
C3743AE3170A6BF66500369F3FCE942901A25363
BDDA3A2C5A1BF623B96A938C63007FBEB6E0757F

Challenge 6

Let's tackle the next challenge, by running the riddle executable:

riddle@host01:~$ ./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'
Hint:          'help us play!'.
[1897] PING!
FAIL

Challenge   7: 'What's in a name?'
Hint:          'A rose, by any other name...'.
FAIL

Next challenge locked. Complete more challenges.

Looks like this challenge is a game of PING and PONG, possibly between processes?

Let me find out:

riddle@host01:~$ strace ./riddle
.
.
.
write(2, "\nChallenge   6: 'ping pong'\n", 28
Challenge   6: 'ping pong'
) = 28
write(2, "Hint:          'help us play!'.\n", 32Hint:          'help us play!'.
) = 32
clone(child_stack=0, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7fa078d2e9d0) = 1920
clone(child_stack=0, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7fa078d2e9d0) = 1921
wait4(-1, [{WIFEXITED(s) && WEXITSTATUS(s) == 1}], 0, NULL) = 1921
--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=1921, si_uid=1001, si_status=1, si_utime=0, si_stime=0} ---
wait4(-1, [1920] PING!
[{WIFEXITED(s) && WEXITSTATUS(s) == 1}], 0, NULL) = 1920
--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=1920, si_uid=1001, si_status=1, si_utime=0, si_stime=0} ---
write(2, "\33[31mFAIL\33[0m\n", 14FAIL
)     = 14

Hmmm.

So the riddle executable after printing the challenge title and hint, creates two subprocesses, possibly using the fork(2) system call.

Next, the riddle executable waits for the two child processes to exit.

Indeed, the riddle executable next receives two SIGCHLD signals, one for each terminated child process.

The riddle executable reaps the two children using the wait(2) system call, and the challenge fails.

I can also see a message printed that reads [1920] PING!, which isn't printed by the riddle executable.

The number in the brackets is the same as the return value of the fork(2) system call, so it must be the PID of one of the child processes.

So the child processes do something, let me find out what, by using the -f flag for strace:

riddle@host01:~$ strace -f ./riddle
.
.
.
write(2, "\nChallenge   6: 'ping pong'\n", 28
Challenge   6: 'ping pong'
) = 28
write(2, "Hint:          'help us play!'.\n", 32Hint:          'help us play!'.
) = 32
clone(child_stack=0, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7fdb9c5139d0) = 2282
clone(child_stack=0, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7fdb9c5139d0) = 2283
wait4(-1, strace: Process 2282 attached
strace: Process 2283 attached
 <unfinished ...>
[pid  2283] alarm(2)                    = 0
[pid  2282] fstat(1,  <unfinished ...>
[pid  2283] read(33,  <unfinished ...>
[pid  2282] <... fstat resumed> {st_mode=S_IFCHR|0600, st_rdev=makedev(136, 0), ...}) = 0
[pid  2282] write(1, "[2282] PING!\n", 13 <unfinished ...>
[pid  2283] <... read resumed> 0x7ffcb63c6bec, 4) = -1 EBADF (Bad file descriptor)
[2282] PING!
[pid  2283] alarm(0 <unfinished ...>
[pid  2282] <... write resumed> )       = 13
[pid  2283] <... alarm resumed> )       = 2
[pid  2282] write(34, "\0\0\0\0", 4)    = -1 EBADF (Bad file descriptor)
[pid  2282] exit_group(1)               = ?
[pid  2283] exit_group(1)               = ?
[pid  2283] +++ exited with 1 +++
[pid  2281] <... wait4 resumed> [{WIFEXITED(s) && WEXITSTATUS(s) == 1}], 0, NULL) = 2283
[pid  2281] --- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=2283, si_uid=1001, si_status=1, si_utime=0, si_stime=0} ---
[pid  2281] wait4(-1,  <unfinished ...>
[pid  2282] +++ exited with 1 +++
<... wait4 resumed> [{WIFEXITED(s) && WEXITSTATUS(s) == 1}], 0, NULL) = 2282
--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=2282, si_uid=1001, si_status=1, si_utime=0, si_stime=0} ---
write(2, "\33[31mFAIL\33[0m\n", 14FAIL
)     = 14

I am stil unaware of what is going on, but I can see that the child processes try to read and write to some stragne file descriptors, namely fd=33 and fd=34.

So, although I cannot create such file descriptors for the child processes, I can create such file descriptors for the riddle executrable itself, and let these fds be inherited by the child processes.

Let me try:

riddle@host01:~$ cat strange_fds.py
import os

os.dup2(1, 33)
os.dup2(1, 34)
os.execv("./riddle", ["riddle"])
riddle@host01:~$ strace -f -t python strange_fds.py
.
.
.
10:04:16 write(2, "\nChallenge   6: 'ping pong'\n", 28
Challenge   6: 'ping pong'
) = 28
10:04:16 write(2, "Hint:          'help us play!'.\n", 32Hint:          'help us play!'.
) = 32
10:04:16 clone(child_stack=0, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f598960c9d0) = 2828
strace: Process 2828 attached
[pid  2827] 10:04:16 clone(child_stack=0, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f598960c9d0) = 2829
[pid  2828] 10:04:16 fstat(1,  <unfinished ...>
[pid  2827] 10:04:16 wait4(-1,  <unfinished ...>
[pid  2828] 10:04:16 <... fstat resumed> {st_mode=S_IFCHR|0600, st_rdev=makedev(136, 0), ...}) = 0
[pid  2828] 10:04:16 write(1, "[2828] PING!\n", 13strace: Process 2829 attached
[2828] PING!
) = 13
[pid  2828] 10:04:16 write(34, "\0\0\0\0", 4) = 4
[pid  2828] 10:04:16 alarm(2)           = 0
[pid  2828] 10:04:16 read(53, 0x7ffc4b2160bc, 4) = -1 EBADF (Bad file descriptor)
[pid  2828] 10:04:16 alarm(0)           = 2
[pid  2828] 10:04:16 exit_group(1)      = ?
[pid  2828] 10:04:16 +++ exited with 1 +++
[pid  2827] 10:04:16 <... wait4 resumed> [{WIFEXITED(s) && WEXITSTATUS(s) == 1}], 0, NULL) = 2828
[pid  2827] 10:04:16 --- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=2828, si_uid=1001, si_status=1, si_utime=0, si_stime=0} ---
[pid  2827] 10:04:16 wait4(-1,  <unfinished ...>
[pid  2829] 10:04:16 alarm(2)           = 0
[pid  2829] 10:04:16 read(33, 0x7ffc4b2160bc, 4) = ? ERESTARTSYS (To be restarted if SA_RESTART is set)
[pid  2829] 10:04:18 --- SIGALRM {si_signo=SIGALRM, si_code=SI_KERNEL} ---
[pid  2829] 10:04:18 +++ killed by SIGALRM +++
10:04:18 <... wait4 resumed> [{WIFSIGNALED(s) && WTERMSIG(s) == SIGALRM}], 0, NULL) = 2829
10:04:18 --- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_KILLED, si_pid=2829, si_uid=1001, si_status=SIGALRM, si_utime=0, si_stime=0} ---
10:04:18 write(2, "\33[31mFAIL\33[0m\n", 14FAIL
) = 14

Still, unsure what is going on.

Let me follow just the child processes separately.

The first child has PID=2828.

  • Initially, it calls fstat(2) on fd=1 which is STDOUT, which succeedes
  • Then, it writes a message to STDOUT containing its PID and PING!
  • Next, it writes 4 NULL bytes \0\0\0\0 to file descriptor fd=34
  • Then, it sets up an SIGALRM signal to be delivered to it in 2 seconds
  • Next, it tries to read 4 bytes from file descriptor fd=53 and fails, because no such file descriptor exists
  • Then, it calls alarm(2) with 0 as argument and so it immediately unsets any previous alarms that have been setup.
  • Finally, it calls exit(2) with 1 as argument to indicate an unsuccessful status code.

The second child has PID=2829.

  • Initially, it calls alarm() and sets up a SIGALRM signal to be delivered 2 seconds in the future
  • Then, it tries to read 4 bytes from the file descriptor fd=33
  • The read(2) system call blocks ...
  • Next the SIGALRM signal is received, and the second child is killed by it

And of course the parent process, the riddle executable reaps the exited children.

OK, so, the big picture is:

  • Child 1 writes 4 bytes to fd=34 and reads 4 bytes from fd=53
  • Child 2 reads 4 bytes from fd=33 (and possibly writes 4 bytes to fd=54?)

I believe this read-write game among the child processes explains the "ping-pong" name of this challenge.

So, I must ensure:

  • The child processes have the file descriptors they need
  • What one child process writes, the other child process can read

Pipes to the rescue!

I will create one pipe with fd=34 for writing, fd=33 for reading. And one more pipe with fd=53 for reading and fd=54 for writing.

Let me see:

riddle@host01:~$ cat strange_fds.py
import os

r, w = os.pipe()
os.dup2(r, 33)
os.dup2(w, 34)

r, w = os.pipe()
os.dup2(r, 53)
os.dup2(w, 54)

os.execv("./riddle", ["riddle"])
riddle@host01:~$ python strange_fds.py

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'
Hint:          'help us play!'.
[2378] PING!
[2379] PONG!
[2379] PING!
[2378] PONG!
[2378] PING!
[2379] PONG!
[2379] PING!
[2378] PONG!
[2378] PING!
[2379] PONG!
[2379] PING!
[2378] PONG!
[2378] PING!
[2379] PONG!
[2379] PING!
[2378] PONG!
[2378] PING!
[2379] PONG!
[2379] PING!
[2378] PONG!
[2378] PING!
[2379] PONG!
[2379] PING!
[2378] PONG!
SUCCESS

Challenge   7: 'What's in a name?'
Hint:          'A rose, by any other name...'.
FAIL

Challenge   8: 'Big Data'
Hint:          'Checking footers'.
Data files must be present and whole. FAIL

Next challenge locked. Complete more challenges.
riddle@host01:~$

Voila! The riddle process, created two child processes, that communicated over two anonymous pipes and exchanged PING-PONG messages. Therefore, the riddle executrable, succeeded, and moved on.

Challenge 6 passed!

riddle@host01:~$ cat riddle.savegame
56BA6BB80350DF234409301C755F155931C3104C
D5AB23E8FA85E1B93CFB9073B635DAFA39DC1520
12BABA77EB6CB7DA6921CAEAEBC8BFB15F15E1AD
4B95AA1F5D6F3980F888F0B3FD84070A356AD8FA
C3743AE3170A6BF66500369F3FCE942901A25363
BDDA3A2C5A1BF623B96A938C63007FBEB6E0757F
2620B82CA4CC577DE086EDC3E5F81213BBB98E26

Challenge 7

Let's tackle the next challenge, by running the riddle executable:

riddle@host01:~$ ./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?'
Hint:          'A rose, by any other name...'.
FAIL

Challenge   8: 'Big Data'
Hint:          'Checking footers'.
Data files must be present and whole. FAIL

Next challenge locked. Complete more challenges.

OK, nothing interesting so far, let's look closer with strace:

riddle@host01:~$ strace ./riddle
.
.
.
write(2, "\nChallenge   7: 'What's in a nam"..., 36
Challenge   7: 'What's in a name?'
) = 36
write(2, "Hint:          'A rose, by any o"..., 47Hint:          'A rose, by any other name...'.
) = 47
lstat(".hello_there", 0x7ffd5edb5180)   = -1 ENOENT (No such file or directory)
write(2, "\33[31mFAIL\33[0m\n", 14FAIL
)     = 14

So, the riddle executable tried to find the .hello_there file, but failed.

Let me try again:

riddle@host01:~$ touch .hello_there
riddle@host01:~$ strace ./riddle
.
.
.
write(2, "\nChallenge   7: 'What's in a nam"..., 36
Challenge   7: 'What's in a name?'
) = 36
write(2, "Hint:          'A rose, by any o"..., 47Hint:          'A rose, by any other name...'.
) = 47
lstat(".hello_there", {st_mode=S_IFREG|0664, st_size=0, ...}) = 0
lstat(".hey_there", 0x7ffc63d68ef0)     = -1 ENOENT (No such file or directory)
write(2, "\33[31mFAIL\33[0m\n", 14FAIL
)     = 14

This time, the riddle executable tried to find the .hey_there file.

Let me try again:

riddle@host01:~$ touch .hey_there
riddle@host01:~$ strace ./riddle
.
.
.
write(2, "\nChallenge   7: 'What's in a nam"..., 36
Challenge   7: 'What's in a name?'
) = 36
write(2, "Hint:          'A rose, by any o"..., 47Hint:          'A rose, by any other name...'.
) = 47
lstat(".hello_there", {st_mode=S_IFREG|0664, st_size=0, ...}) = 0
lstat(".hey_there", {st_mode=S_IFREG|0664, st_size=0, ...}) = 0
write(2, "Oops. 262113 != 262127.\n", 24Oops. 262113 != 262127.
) = 24
write(2, "\33[31mFAIL\33[0m\n", 14FAIL
)     = 14

Hmmm.

I failed the challenge with the message: Oops. 262113 != 262127.

Interesting!

The riddle executable seems to compare two values after calling stat(2) on two different files.

The stat(2) system call retrieves information about a file.

So the two compared values must relate to some file information.

Let me run the stat CLI tool to inspect the same information for those two files:

riddle@host01:~$ stat .hello_there
  File: '.hello_there'
  Size: 0               Blocks: 0          IO Block: 4096   regular empty file
Device: fd01h/64769d    Inode: 262113      Links: 1
Access: (0664/-rw-rw-r--)  Uid: ( 1001/  riddle)   Gid: ( 1001/  riddle)
Access: 2020-03-18 14:49:35.996000000 +0000
Modify: 2020-03-18 14:49:35.996000000 +0000
Change: 2020-03-18 14:49:35.996000000 +0000
 Birth: -
riddle@host01:~$ stat .hey_there
  File: '.hey_there'
  Size: 0               Blocks: 0          IO Block: 4096   regular empty file
Device: fd01h/64769d    Inode: 262127      Links: 1
Access: (0664/-rw-rw-r--)  Uid: ( 1001/  riddle)   Gid: ( 1001/  riddle)
Access: 2020-03-18 14:49:39.964000000 +0000
Modify: 2020-03-18 14:49:39.964000000 +0000
Change: 2020-03-18 14:49:39.964000000 +0000
 Birth: -

There it is!

The two compared values are the file inodes!

So, to pass the challenge, I must make the two files have the same inode.

The inode is unique for each file, so .hello_there and .hey_there can only point to the same inode if they are symlinks to the same file:

riddle@host01:~$ rm .hey_there
riddle@host01:~$ link .hello_there .hey_there
riddle@host01:~$ stat .hello_there
  File: '.hello_there'
  Size: 0               Blocks: 0          IO Block: 4096   regular empty file
Device: fd01h/64769d    Inode: 262113      Links: 2
Access: (0664/-rw-rw-r--)  Uid: ( 1001/  riddle)   Gid: ( 1001/  riddle)
Access: 2020-03-18 14:49:35.996000000 +0000
Modify: 2020-03-18 14:49:35.996000000 +0000
Change: 2020-03-18 14:53:18.696000000 +0000
 Birth: -
riddle@host01:~$ stat .hey_there
  File: '.hey_there'
  Size: 0               Blocks: 0          IO Block: 4096   regular empty file
Device: fd01h/64769d    Inode: 262113      Links: 2
Access: (0664/-rw-rw-r--)  Uid: ( 1001/  riddle)   Gid: ( 1001/  riddle)
Access: 2020-03-18 14:49:35.996000000 +0000
Modify: 2020-03-18 14:49:35.996000000 +0000
Change: 2020-03-18 14:53:18.696000000 +0000
 Birth: -

And so:

riddle@host01:~$ ./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?'
Hint:          'A rose, by any other name...'.
...would smell as sweet. SUCCESS

Challenge   8: 'Big Data'
Hint:          'Checking footers'.
Data files must be present and whole. FAIL

Challenge   9: 'Connect'
Hint:          'Let me whisper in your ear'.
I am trying to contact you...\n...but you don't seem to listen.\nFAIL

Next challenge locked. Complete more challenges.

Voila! The riddle process, found the same inode for these two files, succeeded, and moved on.

Challenge 7 passed!

riddle@host01:~$ cat riddle.savegame
56BA6BB80350DF234409301C755F155931C3104C
D5AB23E8FA85E1B93CFB9073B635DAFA39DC1520
12BABA77EB6CB7DA6921CAEAEBC8BFB15F15E1AD
4B95AA1F5D6F3980F888F0B3FD84070A356AD8FA
C3743AE3170A6BF66500369F3FCE942901A25363
BDDA3A2C5A1BF623B96A938C63007FBEB6E0757F
2620B82CA4CC577DE086EDC3E5F81213BBB98E26
6B413C2F8B7E9E8CE3013FEBCD4FE61B3B9CDE13

Challenge 8

Let's tackle the next challenge, by running the riddle executable:

riddle@host01:~$ ./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'
Hint:          'Checking footers'.
Data files must be present and whole. FAIL

Challenge   9: 'Connect'
Hint:          'Let me whisper in your ear'.
I am trying to contact you...\n...but you don't seem to listen.\nFAIL

Next challenge locked. Complete more challenges.

OK, nothing interesting so far, let's look closer with strace:

riddle@host01:~$ strace ./riddle
.
.
.
write(2, "\nChallenge   8: 'Big Data'\n", 27
Challenge   8: 'Big Data'
) = 27
write(2, "Hint:          'Checking footers"..., 35Hint:          'Checking footers'.
) = 35
open("bf00", O_RDONLY)                  = -1 ENOENT (No such file or directory)
open("bf00", O_RDONLY)                  = -1 ENOENT (No such file or directory)
open("bf00", O_RDONLY)                  = -1 ENOENT (No such file or directory)
open("bf00", O_RDONLY)                  = -1 ENOENT (No such file or directory)
open("bf00", O_RDONLY)                  = -1 ENOENT (No such file or directory)
open("bf00", O_RDONLY)                  = -1 ENOENT (No such file or directory)
open("bf00", O_RDONLY)                  = -1 ENOENT (No such file or directory)
open("bf00", O_RDONLY)                  = -1 ENOENT (No such file or directory)
open("bf00", O_RDONLY)                  = -1 ENOENT (No such file or directory)
open("bf00", O_RDONLY)                  = -1 ENOENT (No such file or directory)
write(2, "\33[31mData files must be present "..., 52Data files must be present and whole. FAIL
) = 52

OK, so the riddle executable tries to open(2) a file called bf00.

It fails, because such file doesn't exist.

Let me create it:

riddle@host01:~$ touch bf00
riddle@host01:~$ strace ./riddle
.
.
.
write(2, "\nChallenge   8: 'Big Data'\n", 27
Challenge   8: 'Big Data'
) = 27
write(2, "Hint:          'Checking footers"..., 35Hint:          'Checking footers'.
) = 35
open("bf00", O_RDONLY)                  = 4
lseek(4, 1073741824, SEEK_SET)          = 1073741824
read(4, "", 16)                         = 0
fstat(1, {st_mode=S_IFCHR|0600, st_rdev=makedev(136, 0), ...}) = 0
write(1, "X", 1X)                        = 1
close(4)                                = 0
open("bf00", O_RDONLY)                  = 4
lseek(4, 1073741824, SEEK_SET)          = 1073741824
read(4, "", 16)                         = 0
write(1, "X", 1X)                        = 1
close(4)                                = 0
open("bf00", O_RDONLY)                  = 4
lseek(4, 1073741824, SEEK_SET)          = 1073741824
read(4, "", 16)                         = 0
write(1, "X", 1X)                        = 1
close(4)                                = 0
open("bf00", O_RDONLY)                  = 4
lseek(4, 1073741824, SEEK_SET)          = 1073741824
read(4, "", 16)                         = 0
write(1, "X", 1X)                        = 1
close(4)                                = 0
open("bf00", O_RDONLY)                  = 4
lseek(4, 1073741824, SEEK_SET)          = 1073741824
read(4, "", 16)                         = 0
write(1, "X", 1X)                        = 1
close(4)                                = 0
open("bf00", O_RDONLY)                  = 4
lseek(4, 1073741824, SEEK_SET)          = 1073741824
read(4, "", 16)                         = 0
write(1, "X", 1X)                        = 1
close(4)                                = 0
open("bf00", O_RDONLY)                  = 4
lseek(4, 1073741824, SEEK_SET)          = 1073741824
read(4, "", 16)                         = 0
write(1, "X", 1X)                        = 1
close(4)                                = 0
open("bf00", O_RDONLY)                  = 4
lseek(4, 1073741824, SEEK_SET)          = 1073741824
read(4, "", 16)                         = 0
write(1, "X", 1X)                        = 1
close(4)                                = 0
open("bf00", O_RDONLY)                  = 4
lseek(4, 1073741824, SEEK_SET)          = 1073741824
read(4, "", 16)                         = 0
write(1, "X", 1X)                        = 1
close(4)                                = 0
open("bf00", O_RDONLY)                  = 4
lseek(4, 1073741824, SEEK_SET)          = 1073741824
read(4, "", 16)                         = 0
write(1, "X", 1X)                        = 1
close(4)                                = 0
write(2, "\33[31mData files must be present "..., 52Data files must be present and whole. FAIL
) = 52

Woah! That's a lot of output...

Let start at the top.

The riddle executable calls open(2) on the bf00 file, using the O_RDONLY flag, and obtains the file descriptor fd=4.

It then calls lseek(2) to reposition the file offset on the file descriptor fd=4. It passes the SEEK_SET flag to directly set the offet to value 1073741824.

Next it reads 16 bytes from file descriptor fd=4.

The file bf00 was initially empty, and after the lseek(2) syscall the file description offeset was moved "definitely" past the EOF, therefore extending the original size of the file.

But at that offset, nothing exists but the EOF, and so the read(2) syscall returns 0 bytes.

The riddle executable then writes a X char on STDOUT and repeats.

The cycle is repeated 10 times.

I am tempted to create the bf00 file, and make it big enough, and then write a single character at the end, so as the read(2) syscall at offset 1073741824 will not fail:

riddle@host01:~$ truncate -s 1073741824 bf00
riddle@host01:~$ echo -n A >> bf00
riddle@host01:~$ hexdump bf00
0000000 0000 0000 0000 0000 0000 0000 0000 0000
*
40000000 0041
40000001
riddle@host01:~$ strace ./riddle
.
.
.
write(2, "\nChallenge   8: 'Big Data'\n", 27
Challenge   8: 'Big Data'
) = 27
write(2, "Hint:          'Checking footers"..., 35Hint:          'Checking footers'.
) = 35
open("bf00", O_RDONLY)                  = 4
lseek(4, 1073741824, SEEK_SET)          = 1073741824
read(4, "A", 16)                        = 1
fstat(1, {st_mode=S_IFCHR|0600, st_rdev=makedev(136, 0), ...}) = 0
write(1, ".", 1.)                        = 1
close(4)                                = 0
open("bf01", O_RDONLY)                  = -1 ENOENT (No such file or directory)
open("bf01", O_RDONLY)                  = -1 ENOENT (No such file or directory)
open("bf01", O_RDONLY)                  = -1 ENOENT (No such file or directory)
open("bf01", O_RDONLY)                  = -1 ENOENT (No such file or directory)
open("bf01", O_RDONLY)                  = -1 ENOENT (No such file or directory)
open("bf01", O_RDONLY)                  = -1 ENOENT (No such file or directory)
open("bf01", O_RDONLY)                  = -1 ENOENT (No such file or directory)
open("bf01", O_RDONLY)                  = -1 ENOENT (No such file or directory)
open("bf01", O_RDONLY)                  = -1 ENOENT (No such file or directory)
write(2, "\33[31mData files must be present "..., 52Data files must be present and whole. FAIL
) = 52

There you go!

But now it wants the bf01 file!

I don't like the idea of creating every single file that it wants.

I think that would take TOO much disk space.

Instead, I am thinking about symlinking bf00 to bfXX ;)

Let me try it:

riddle@host01:~$ for i in {1..10} ; do link bf00 bf$(printf "%.2d" $i) ; done
riddle@host01:~$ ./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'
Hint:          'Checking footers'.
..........SUCCESS

Challenge   9: 'Connect'
Hint:          'Let me whisper in your ear'.
I am trying to contact you...\n...but you don't seem to listen.\nFAIL

Challenge  10: 'ESP'
Hint:          'Can you read my mind?'.
What hex number am I thinking of right now? ^C

Voila! The riddle process, found all the "big files" it wanted succeeded, and moved on.

Challenge 8 passed!

riddle@host01:~$ cat riddle.savegame
56BA6BB80350DF234409301C755F155931C3104C
D5AB23E8FA85E1B93CFB9073B635DAFA39DC1520
12BABA77EB6CB7DA6921CAEAEBC8BFB15F15E1AD
4B95AA1F5D6F3980F888F0B3FD84070A356AD8FA
C3743AE3170A6BF66500369F3FCE942901A25363
BDDA3A2C5A1BF623B96A938C63007FBEB6E0757F
2620B82CA4CC577DE086EDC3E5F81213BBB98E26
6B413C2F8B7E9E8CE3013FEBCD4FE61B3B9CDE13
8E5F111E300EF6CF2854FAF86C6852041D6EB23C

Of course, what I didn't know, and just found out is that the filesystem is usually clever: if part of a file has never been written to (and is therefore all zeros), it doesn't bother to allocate any space for it.

Using truncate, I have created what is known as a "sparse file": a file that, because most of it is empty (i.e. reads back as \0), doesn't take space on the disk besides what is actually written: that is 1B, after 1GB of gap.

So after all, I could have created all of bfXX files, it wouldn't have filled my disk ;)

Challenge 9

Let's tackle the next challenge, by running the riddle executable:

riddle@host01:~$ ./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'
Hint:          'Let me whisper in your ear'.
I am trying to contact you...\n...but you don't seem to listen.\nFAIL

Challenge  10: 'ESP'
Hint:          'Can you read my mind?'.
What hex number am I thinking of right now? ^C

OK, the riddle executable says it is trying to contact me, but I don't seem to listen.

Let me use strace:

riddle@host01:~$ strace ./riddle
.
.
.
write(2, "\nChallenge   9: 'Connect'\n", 26
Challenge   9: 'Connect'
) = 26
write(2, "Hint:          'Let me whisper i"..., 45Hint:          'Let me whisper in your ear'.
) = 45
socket(PF_INET, SOCK_STREAM, IPPROTO_IP) = 4
write(2, "I am trying to contact you...\\n", 31I am trying to contact you...\n) = 31
connect(4, {sa_family=AF_INET, sin_port=htons(49842), sin_addr=inet_addr("127.0.0.1")}, 16) = -1 ECONNREFUSED (Connection refused)
write(2, "...but you don't seem to listen."..., 34...but you don't seem to listen.\n) = 34
write(2, "\33[31mFAIL\33[0m\n", 14FAIL
)     = 14

The riddle executable creates a socket with the socket(2) system call that can be used.

The open socket returns the file descriptor fd=4.

Then the riddle executable attempts to use this open socket to connect to localhost port 49842.

So I will write a python program that will call fork(2).

The parent will listen on this TCP port, while the child will run the riddle executable:

riddle@host01:~$ cat ch9.py
import os
import sys
import time
import socket


if __name__ == "__main__":
    time.sleep(3)
    pid = os.fork()
    if pid != 0:
        sys.stderr.write("PARENT: This is the parent\n")
        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        s.bind(("127.0.0.1", 49842))
        s.listen(5)
        sys.stderr.write("PARENT: Listening on 127.0.0.1 port 49842\n")
        while True:
            (clientsocket, address) = s.accept()
            data = clientsocket.recv(4096)
            sys.stderr.write("PARENT: Received data: ")
            sys.stderr.write(data)
            sys.stderr.write("\n")
    else:
        time.sleep(5)
        os.execv("./riddle", ["riddle"])
riddle@host01:~$ python ch9.py
PARENT: This is the parent
PARENT: Listening on 127.0.0.1 port 49842

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'
Hint:          'Let me whisper in your ear'.
I am trying to contact you...\nPARENT: Received data: How much is 20682 + 1?

... and it freezes there.

I guess I must reply back with the correct answer.

riddle@host01:~$ cat ch9.py
import os
import sys
import time
import socket


if __name__ == "__main__":
    time.sleep(1)
    pid = os.fork()
    if pid != 0:
        sys.stderr.write("PARENT: This is the parent\n")
        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        s.bind(("127.0.0.1", 49842))
        s.listen(5)
        sys.stderr.write("PARENT: Listening on 127.0.0.1 port 49842\n")
        while True:
            (clientsocket, address) = s.accept()
            data = clientsocket.recv(4096)
            sys.stderr.write("PARENT: Received data: ")
            sys.stderr.write(data)
            sys.stderr.write("\n")
            time.sleep(1)
            expression = data[len("How much is "):]
            expression = expression.split("?")[0]
            sys.stderr.write("PARENT: The expression is: `")
            sys.stderr.write(expression)
            sys.stderr.write("`\n")
            time.sleep(1)
            clientsocket.send(str(eval(expression)))
    else:
        time.sleep(3)
        os.execv("./riddle", ["riddle"])
riddle@host01:~$ python ch9.py
PARENT: This is the parent
PARENT: Listening on 127.0.0.1 port 49842

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'
Hint:          'Let me whisper in your ear'.
I am trying to contact you...\nPARENT: Received data: How much is 2303 + 1?
PARENT: The expression is: `2303 + 1`
SUCCESS

Challenge  10: 'ESP'
Hint:          'Can you read my mind?'.
What hex number am I thinking of right now?
You're only guessing, and I am wasting my time.\nFAIL

Challenge  11: 'ESP-2'
Hint:          'Can you read my mind?'.
What hex number am I thinking of right now?
You're only guessing, and I am wasting my time.\nFAIL

Next challenge locked. Complete more challenges.

Voila! The riddle process received the correct answer, succeeded, and moved on.

Challenge 9 passed!

riddle@host01:~$ cat riddle.savegame
56BA6BB80350DF234409301C755F155931C3104C
D5AB23E8FA85E1B93CFB9073B635DAFA39DC1520
12BABA77EB6CB7DA6921CAEAEBC8BFB15F15E1AD
4B95AA1F5D6F3980F888F0B3FD84070A356AD8FA
C3743AE3170A6BF66500369F3FCE942901A25363
BDDA3A2C5A1BF623B96A938C63007FBEB6E0757F
2620B82CA4CC577DE086EDC3E5F81213BBB98E26
6B413C2F8B7E9E8CE3013FEBCD4FE61B3B9CDE13
8E5F111E300EF6CF2854FAF86C6852041D6EB23C
9E7FFAC47A530B3DE42490CD656BBEB8C48687BA

Challenge 10

Let's tackle the next challenge, by running the riddle executable:

riddle@host01:~$ ./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'
Hint:          'Can you read my mind?'.
What hex number am I thinking of right now?

And the riddle executable waits for input.

Let me give 0 as an answer and see what happens:

Challenge  10: 'ESP'
Hint:          'Can you read my mind?'.
What hex number am I thinking of right now? 0
You're only guessing, and I am wasting my time.\nFAIL

Yeah, sure, I am only guessing and of course I guessed wrong.

Let me use strace once more:

riddle@host01:~$ strace ./riddle
.
.
.
write(2, "\nChallenge  10: 'ESP'\n", 22
Challenge  10: 'ESP'
) = 22
write(2, "Hint:          'Can you read my "..., 40Hint:          'Can you read my mind?'.
) = 40
open("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) = 0x7fa1eaf51000
close(4)                                = 0
write(2, "What hex number am I thinking of"..., 44What hex number am I thinking of right now? ) = 44
fstat(0, {st_mode=S_IFCHR|0600, st_rdev=makedev(136, 0), ...}) = 0
read(0,

And it waits for input.

So, first it calls open(2) to open a file named secret_number, and using the flags O_RDWR | O_CREAT | O_TRUNC it also creates it since it doesn't already exists.

The open(2) system call returns the file descriptor fd=4.

The riddle executable then calls unlink(2) on the secret_number file.

AFAIK, this file does not have any links pointing to it, so the unlink(2) system call would normally remove this file.

But, since the riddle process has already opened this file, the file will remain in existence until the last file descriptor referring to it is closed.

Then the riddle executable writes its secret number to that file.

This is what I must read ...

Next it calls mmap(2) and maps the contents of the open file identified by file descriptor fd=4 (which is the secret_number file) to memory.

Specifically, it requests the file contents at offset=0 and the next length=4096 bytes to be mapped to a random virtual memory address. The address is returned by the mmap(2) call.

The PROT_READ | PROT_WRITE flags indicate that the process will try to both read and write the mapped memory area.

The most important argument passed to mmap(2) is MAP_SHARED which instructs the kernel to "share this mapping".

This means that any changes made by the process to the mapped virtual memory, will also be resembled to: both other processes that have also mapped this file, as well as to the file on the filesystem.

In my case, the underlying file on the filesystem is no longer available, after the unlink(2) system call.

So, even if the riddle process would write the answer to the mapped memory area, I couldn't read this value off from a filesystem file.

Therefore, I think that I must access take access to the pages that hold the secret_number file contents: the pages that are mapped to the riddle process virtual memory.

I think that the only way to have access to these pages, is if I have another process (which I can control) that also has these pages mapped into its own virtual memory.

So, let me create two processes, one parent and one child.

The parent process will create the secret_number file (before the child), and keep it open to read it later ;)

The child process will run the riddle executable, will open the secret_number file (which will already exist), and write in the answer.

The parent process would then keep on posting the contents of the secret_number file to STDOUT for me to read ;)

riddle@host01:~$ cat secret_number.py
import os
import time

pid = os.fork()
if pid:
    # parent
    with open("secret_number", "w+") as f:
        while True:
            time.sleep(1)
            data = f.read()
            if len(data) != 0:
                print
                print "PARENT: The contents are: " + data
else:
    # child
    time.sleep(3)
    os.execv("./riddle", ["riddle"])

Let me try this out:

riddle@host01:~$ python secret_number.py

PARENT: The contents are:

PARENT: The contents are:

PARENT: The contents are:

PARENT: The contents are:

Challenge   0: 'Hello there'

PARENT: The contents are:
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'
Hint:          'Can you read my mind?'.
What hex number am I thinking of right now?
PARENT: The contents are: The number I am thinking of right now is [uppercase matters]: 8DB797C2

GREAT!

I guess I could write some more python code, to get this answer fed back into the riddle process, but I am too lazy.

I will just copy and paste the answer into the terminal, where the riddle executable is waiting for my input in its STDIN.

Question: How I know which process will "consume" what I type into the terminal? There are two processes right? I only want to send bytes to the STDIN of only one of them, the riddle. Hmmm. There must be some UNIX console magic taking place here that I don't quite get.

Answer: Thanks to Gilles. It's possible to feed input to multiple processes. When multiple processes are reading from the same pipe or terminal, each byte goes to one of the processes, whichever happens to read that particular byte first. When only one process is actively reading, it gets the input. When multiple processes are actively reading at the same time, which one gets the input is unpredictable.

So, I guess I am OK in this situtation, since only the riddle process is actually reading from STDIN. The other process is not.

In any case:

Challenge  10: 'ESP'
Hint:          'Can you read my mind?'.
What hex number am I thinking of right now?
PARENT: The contents are: The number I am thinking of right now is [uppercase matters]: 37543927

37543927
SUCCESS

Voila! The riddle process received the correct answer, succeeded, and moved on.

Challenge 10 passed!

riddle@host01:~$ cat riddle.savegame
56BA6BB80350DF234409301C755F155931C3104C
D5AB23E8FA85E1B93CFB9073B635DAFA39DC1520
12BABA77EB6CB7DA6921CAEAEBC8BFB15F15E1AD
4B95AA1F5D6F3980F888F0B3FD84070A356AD8FA
C3743AE3170A6BF66500369F3FCE942901A25363
BDDA3A2C5A1BF623B96A938C63007FBEB6E0757F
2620B82CA4CC577DE086EDC3E5F81213BBB98E26
6B413C2F8B7E9E8CE3013FEBCD4FE61B3B9CDE13
8E5F111E300EF6CF2854FAF86C6852041D6EB23C
9E7FFAC47A530B3DE42490CD656BBEB8C48687BA
DDF31257B0E96C1A2F7257A97BC262C801882C9B

Challenge 11

Let's tackle the next challenge, by running the riddle executable:

riddle@host01:~$ ./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'
Hint:          'Can you read my mind?'.
What hex number am I thinking of right now?

And the riddle executable waits for input.

Same as before.

Let me use strace again:

riddle@host01:~$ strace ./riddle
.
.
.
write(2, "\nChallenge  11: 'ESP-2'\n", 24
Challenge  11: 'ESP-2'
) = 24
write(2, "Hint:          'Can you read my "..., 40Hint:          'Can you read my mind?'.
) = 40
open("secret_number", O_RDWR|O_CREAT|O_TRUNC, 0600) = 4
unlink("secret_number")                 = 0
fstat(4, {st_mode=S_IFREG|0600, st_size=0, ...}) = 0
write(4, "The number I am thinking of righ"..., 4096) = 4096
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_SHARED, 4, 0) = 0x7f279fb4b000
close(4)                                = 0
write(2, "What hex number am I thinking of"..., 44What hex number am I thinking of right now? ) = 44
fstat(0, {st_mode=S_IFCHR|0600, st_rdev=makedev(136, 0), ...}) = 0
read(0,

This looks extremely similar to the previous challenge, with the difference that this time the riddle executable also calls fstat(2) on the secret_number file.

Let me use the same approach as before:

riddle@host01:~$ python secret_number.py

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'
Hint:          'Can you read my mind?'.
What hex number am I thinking of right now?
PARENT: The contents are: The number I am thinking of right now is [uppercase matters]: 1B53DFD0

1B53DFD0
SUCCESS

Oops!

This was too easy!

I must have bypassed the actual game logic here and used some kind of shortcut...

Anyway! :P

Voila! The riddle process received the correct answer, succeeded, and moved on.

Challenge 11 passed!

riddle@host01:~$ cat riddle.savegame
56BA6BB80350DF234409301C755F155931C3104C
D5AB23E8FA85E1B93CFB9073B635DAFA39DC1520
12BABA77EB6CB7DA6921CAEAEBC8BFB15F15E1AD
4B95AA1F5D6F3980F888F0B3FD84070A356AD8FA
C3743AE3170A6BF66500369F3FCE942901A25363
BDDA3A2C5A1BF623B96A938C63007FBEB6E0757F
2620B82CA4CC577DE086EDC3E5F81213BBB98E26
6B413C2F8B7E9E8CE3013FEBCD4FE61B3B9CDE13
8E5F111E300EF6CF2854FAF86C6852041D6EB23C
9E7FFAC47A530B3DE42490CD656BBEB8C48687BA
DDF31257B0E96C1A2F7257A97BC262C801882C9B
1C1B6837401D1CF20CDD2C7F92240680D5FB6F45

Challenge 12

Let's tackle the next challenge, by running the riddle executable:

riddle@host01:~$ ./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'
Hint:          'Do only what is required, nothing more, nothing less'.
I want to find the char 'Z' at 0x7fdf8d95a06f

And the riddle process hangs there.

Let me use strace:

riddle@host01:~$ strace ./riddle
.
.
.
write(2, "\nChallenge  12: 'A delicate chan"..., 36
Challenge  12: 'A delicate change'
) = 36
write(2, "Hint:          'Do only what is "..., 71Hint:          'Do only what is required, nothing more, nothing less'.
) = 71
getpid()                                = 19449
open("/tmp/riddle-5bPU5s", O_RDWR|O_CREAT|O_EXCL, 0600) = 4
ftruncate(4, 4096)                      = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_SHARED, 4, 0) = 0x7fa34ddb0000
write(2, "I want to find the char 'Q' at 0"..., 46I want to find the char 'Q' at 0x7fa34ddb006f
) = 46
nanosleep({2, 0}, 0x7ffed557b1a0)       = 0
nanosleep({2, 0}, 0x7ffed557b1a0)       = 0
nanosleep({2, 0}, 0x7ffed557b1a0)       = 0
nanosleep({2, 0}, 0x7ffed557b1a0)       = 0
nanosleep({2, 0}, 0x7ffed557b1a0)       = 0
nanosleep({2, 0}, 0x7ffed557b1a0)       = 0
nanosleep({2, 0}, 0x7ffed557b1a0)       = 0
nanosleep({2, 0}, 0x7ffed557b1a0)       = 0
close(4)                                = 0
unlink("/tmp/riddle-5bPU5s")            = 0
write(2, "\33[31mFAIL\33[0m\n", 14FAIL
)     = 14

So, the riddle process didn't actually hang, it was sleeping.

So, I have a time window to solve this challenge.

The riddle process is opening a file. The file name looks random. Indeed the riddle process had just called getpid(), so it makes sense that the file name is chosen randomly, based on the random PID of the riddle process.

Then the riddle process calls ftruncate(2) to "resize" the file to 4096 bytes (which should be 1 page, I guess).

Next it maps this page onto its virtual memory using mmap(2).

Then it writes a message, letting me know that it wants to find a specific character at a specific address.

By running the riddle process many times under strace and inspecting the virtual address returned by mmap(2) as well as the address that the character must be found, I can infer:

  • The character is random
  • The address returned by mmap(2) is random
  • The requested address is always 0x6f or 111 bytes after the address returned by mmap(2).

So, what I have to do, is put the given character 111 bytes "into" the memory-mapped page.

But this page is allocated in runtime, to contain the contents of a file, whose name isn't known before-hand.

What I notice, is that this time, the file is present in the filesystem, as it is not immediately unlink(2)ed after being open(2)ed.

So, an "ugly" solution would be to exploit the "sleep window" of the riddle process. During that window, I can edit the temporary random-named file, and thus the pages mapped onto the riddles process memory, and write the specific character to the correct position.

This is the "driver" program in Python:

riddle@host01:~$ cat edit_tmp_file.py
import os
import time

pid = os.fork()
if pid:
    # parent
    tmpfile = raw_input()
    print "PARENT: tmpfile is `" + tmpfile + "`"
    fd = os.open(tmpfile, os.O_RDWR)
    os.lseek(fd, 111, os.SEEK_SET)
    character = raw_input()
    print "PARENT: character is `" + character + "`"
    os.write(fd, character)
    os.close(fd)
else:
    # child
    os.execv("./riddle", ["riddle"])

And this is what I use "interactively":

riddle@host01:~$ strace -f python edit_tmp_file.py

The strace program with the -f flag allows me to inspect which file is created by the riddle process.

By copying and pasting this file in the terminal, I feed this value to the "parent" process, which in turn opens this file.

By inspecting the riddle output, I also paste the required charater in the terminal, and so the "parent" process reads this character and writes it to the lseek(2)ed file.

The output of strace, along with what I have pasted into the terminal, is too dirty to paste here ...

Voila! The riddle process found the correct character, succeeded, and moved on.

Challenge 12 passed!

riddle@host01:~$ cat riddle.savegame
56BA6BB80350DF234409301C755F155931C3104C
D5AB23E8FA85E1B93CFB9073B635DAFA39DC1520
12BABA77EB6CB7DA6921CAEAEBC8BFB15F15E1AD
4B95AA1F5D6F3980F888F0B3FD84070A356AD8FA
C3743AE3170A6BF66500369F3FCE942901A25363
BDDA3A2C5A1BF623B96A938C63007FBEB6E0757F
2620B82CA4CC577DE086EDC3E5F81213BBB98E26
6B413C2F8B7E9E8CE3013FEBCD4FE61B3B9CDE13
8E5F111E300EF6CF2854FAF86C6852041D6EB23C
9E7FFAC47A530B3DE42490CD656BBEB8C48687BA
DDF31257B0E96C1A2F7257A97BC262C801882C9B
1C1B6837401D1CF20CDD2C7F92240680D5FB6F45
7B7DDFF9005846FF3A5E7422AC1A4BD27C245FD8

Challenge 13

Let's tackle the next challenge, by running the riddle executable:

riddle@host01:~$ ./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'
Hint:          'Memquake! Don't lose the pages beneath your something something'.

Bus error
riddle@host01:~$

Not much.

Let me use strace:

riddle@host01:~$ strace ./riddle
.
.
.
write(2, "\nChallenge  13: 'Bus error'\n", 28
Challenge  13: 'Bus error'
) = 28
write(2, "Hint:          'Memquake! Don't "..., 82Hint:          'Memquake! Don't lose the pages beneath your something something'.
) = 82
open(".hello_there", O_RDWR|O_CREAT, 0600) = 4
ftruncate(4, 32768)                     = 0
mmap(NULL, 32768, PROT_READ|PROT_WRITE, MAP_SHARED, 4, 0) = 0x7f61ef941000
ftruncate(4, 16384)                     = 0
read(0,
"\n", 1)                        = 1
--- SIGBUS {si_signo=SIGBUS, si_code=BUS_ADRERR, si_addr=0x7f61ef945000} ---
+++ killed by SIGBUS +++
Bus error

So, the riddle process was killed by SIGBUS.

From the mmap(2) man page:

SIGBUS
Attempted access to a portion of the buffer
that does not correspond to the file
(for example, beyond the end of the file,
including the case where another process has truncated the file).

So the riddle process first memory-maps a file onto its virtual memory, then truncates the file and decreases its size to half, and then is killed by receiving a SIGBUS signal.

From that, I understand that the riddle process attempts to access memory that was initially memory-mapped, but after the file resizing, it is now beyond the end of file.

Indeed as can be seen from the si_addr of the siginfo_t structure, the riddle process accessed the memory address 0x7f61ef945000 which is 16384 after the address returned by mmap(2), namely 0x7f61ef941000.

So the riddle process tried to access the first byte after the end of file, after the file has been halved in size.

As a result, the process received a SIGBUS signal and died.

The solution would be to keep the pages in memory.

The riddle process will pause (wait for input) before accessing the byte beyond the end of file.

So I can exploit this wait to re-truncate the file back to its original size, therefore re-allocating the pages in memory and thus making the memory access valid.

In fact, this can be done even from another process that has also memory mapped the same file!

riddle@host01:~$ cat keep_pages.py
import os
import mmap

FILE=".hello_there"
SIZE=32768

fd = os.open(FILE, os.O_RDWR)
mmap.mmap(fd, SIZE, mmap.MAP_SHARED)

pid = os.fork()
if pid:
    # parent
    while True:
        os.ftruncate(fd, SIZE)
else:
    # child
    os.execv("./riddle", ["riddle"])
riddle@host01:~$ python keep_pages.py

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'
Hint:          'Memquake! Don't lose the pages beneath your something something'.
a
SUCCESS

Challenge  14: 'Are you the One?'
Hint:          'Are you 32767? If not, reincarnate!'.
I don't like my PID, get me another one. FAIL

Next challenge locked. Complete more challenges.

Voila! The riddle process accessed memory that was valid, because the "parent" process kept re-sizing the file to its original size. So the riddle succeeded, and moved on.

Challenge 13 passed!

riddle@host01:~$ cat riddle.savegame
56BA6BB80350DF234409301C755F155931C3104C
D5AB23E8FA85E1B93CFB9073B635DAFA39DC1520
12BABA77EB6CB7DA6921CAEAEBC8BFB15F15E1AD
4B95AA1F5D6F3980F888F0B3FD84070A356AD8FA
C3743AE3170A6BF66500369F3FCE942901A25363
BDDA3A2C5A1BF623B96A938C63007FBEB6E0757F
2620B82CA4CC577DE086EDC3E5F81213BBB98E26
6B413C2F8B7E9E8CE3013FEBCD4FE61B3B9CDE13
8E5F111E300EF6CF2854FAF86C6852041D6EB23C
9E7FFAC47A530B3DE42490CD656BBEB8C48687BA
DDF31257B0E96C1A2F7257A97BC262C801882C9B
1C1B6837401D1CF20CDD2C7F92240680D5FB6F45
7B7DDFF9005846FF3A5E7422AC1A4BD27C245FD8
E248CEC18284B31BCDE93C723218BC23929C99B9

Challenge 14

Let's tackle the next challenge, by running the riddle executable:

riddle@host01:~$ ./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?'
Hint:          'Are you 32767? If not, reincarnate!'.
I don't like my PID, get me another one. FAIL

Next challenge locked. Complete more challenges.

Let me use strace:

riddle@host01:~$ strace ./riddle
.
.
.
write(2, "\nChallenge  14: 'Are you the One"..., 35
Challenge  14: 'Are you the One?'
) = 35
write(2, "Hint:          'Are you 32767? I"..., 54Hint:          'Are you 32767? If not, reincarnate!'.
) = 54
getpid()                                = 3562
write(2, "\33[31mI don't like my PID, get me"..., 55I don't like my PID, get me another one. FAIL
) = 55

OK, so this time the riddle process demands to have a specific PID.

Unfortunately there is no way to directly set a process PID.

So by trial and error, and exploiting the root access on the machine:

root@host01:~# echo 32760 > /proc/sys/kernel/ns_last_pid
riddle@host01:~$ ./riddle
riddle@host01:~$ ./riddle
riddle@host01:~$ ./riddle
riddle@host01:~$ ./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?'
Hint:          'Are you 32767? If not, reincarnate!'.
SUCCESS

Challenge  15: 'Tier2 Gate [everything beyond this point is optional]'
Hint:          'Setup Tier2'.
FAIL

Challenge  16: 'Curiosity'
Hint:          'Somewhere, something incredible is waiting to be known'.
I wanted to find my PID inside a long int at this address. FAIL

Next challenge locked. Complete more challenges.

Voila! At some point, the riddle process was assigned the correct PID, succeeded, and moved on.

Challenge 14 passed!

riddle@host01:~$ cat riddle.savegame
56BA6BB80350DF234409301C755F155931C3104C
D5AB23E8FA85E1B93CFB9073B635DAFA39DC1520
12BABA77EB6CB7DA6921CAEAEBC8BFB15F15E1AD
4B95AA1F5D6F3980F888F0B3FD84070A356AD8FA
C3743AE3170A6BF66500369F3FCE942901A25363
BDDA3A2C5A1BF623B96A938C63007FBEB6E0757F
2620B82CA4CC577DE086EDC3E5F81213BBB98E26
6B413C2F8B7E9E8CE3013FEBCD4FE61B3B9CDE13
8E5F111E300EF6CF2854FAF86C6852041D6EB23C
9E7FFAC47A530B3DE42490CD656BBEB8C48687BA
DDF31257B0E96C1A2F7257A97BC262C801882C9B
1C1B6837401D1CF20CDD2C7F92240680D5FB6F45
7B7DDFF9005846FF3A5E7422AC1A4BD27C245FD8
E248CEC18284B31BCDE93C723218BC23929C99B9
C9494534289DFBDBF844B59D0504494A53A9382F

Copyright © 2018 Vasileios Souleles - Theme Skeleton - Blog Engine Pelican - Powered by Python - Made with ❤ in Vim