Link to archived challenge

Category Difficulty Solves Author
reverse medium 2 sudoBash418

Description

Run the executable and it will give you the flag… eventually. In a year or two.

Think you can manipulate time?

Players are given two files to download: hacking-through-time and hacking-through-time-arm64.

There are also three hints available:

  1. Tracing library calls might help.
  2. This ain’t exploit: nothing’s stopping you from hooking whatever you want.
  3. The hacking-through-time-arm64 file is for ARM64 computers, such as newer Apple devices.
    Most players will want the hacking-through-time file, for x86-64 computers.

Note: I will use the x86-64 hacking-through-time binary in this writeup; however, my analysis and solution are valid for both binaries.

Analysis

We can run file hacking-through-time to verify that the file is a normal dynamically-linked 64-bit executable:

hacking-through-time: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=369cbfa3228a0adf4e9b97d18e2a34ea80f8f65a, for GNU/Linux 4.4.0, stripped

We don’t have any debug information or symbols, and the file is rather large (289kB), so static analysis won’t be easy.

Running the program prints This may take a while... (ETA: ~573 days), and the next line cycles through different characters.
If you wait long enough (a few minutes), it will leave c as the first character and move onto the second.
The stated ETA is no joke: each character takes progressively longer to finish.
We need to do something to speed up the process.

Solution 1 - strace

We can run the program under strace to see the syscalls it makes:

$ strace ./hacking-through-time >/dev/null  # redirect stdout for cleaner output
[... lots of initialization spam ...]
write(1, "This may take a while... (ETA: ~"..., 42) = 42
clock_nanosleep(CLOCK_REALTIME, 0, {tv_sec=0, tv_nsec=400000000}, 0x7ffd87f4e9d0) = 0
clock_nanosleep(CLOCK_REALTIME, 0, {tv_sec=0, tv_nsec=500000000}, 0x7ffd87f4e9d0) = 0
write(1, "U\10", 2)                     = 2
clock_nanosleep(CLOCK_REALTIME, 0, {tv_sec=0, tv_nsec=500000000}, 0x7ffd87f4e9d0) = 0
clock_nanosleep(CLOCK_REALTIME, 0, {tv_sec=0, tv_nsec=500000000}, 0x7ffd87f4e9d0) = 0
write(1, "V\10", 2)                     = 2
clock_nanosleep(CLOCK_REALTIME, 0, {tv_sec=0, tv_nsec=600000000}, 0x7ffd87f4e9d0) = 0
clock_nanosleep(CLOCK_REALTIME, 0, {tv_sec=0, tv_nsec=500000000}, 0x7ffd87f4e9d0) = 0
write(1, "W\10", 2)                     = 2
^C

After process initialization, it’s rather quiet.
Between every character write, it calls clock_nanosleep, which “allows the calling thread to sleep for an interval specified with nanosecond precision.”

Presumably, removing or bypassing these clock_nanosleep calls will speed up the program.

Thankfully, strace has the ability to tamper with syscalls (something I learned while writing this writeup!).

Using the -e inject=... option, we can stub out clock_nanosleep and have it immediately return 0.
2>/dev/null silences the messages from strace.

$ strace -e inject=clock_nanosleep:retval=0 ./hacking-through-time 2>/dev/null
This may take a while... (ETA: ~573 days)
clubeh{0h_h0w_71m3_fl135_wh3n_y0u_h4ck_7h3_m41nfr4m3_ab40440b}

There’s the flag, and this time it only took 600 milliseconds.

Solution 2 - ltrace + LD_PRELOAD

We can run the program under ltrace to watch the library calls it makes.
The -x '*' option is required because… reasons.

$ ltrace -x '*' ./hacking-through-time >/dev/null
[... lots of initialization spam ...]
write@libc.so.6(1, "This may take a while... (ETA: ~"..., 42) = 42
nanosleep@libc.so.6(0x7fffb27cc460, 0x7fffb27cc460, 0, 0 <unfinished ...>
clock_nanosleep@libc.so.6(0, 0, 0x7fffb27cc460, 0x7fffb27cc460) = 0
<... nanosleep resumed> )                                    = 0
nanosleep@libc.so.6(0x7fffb27cc460, 0x7fffb27cc460, 1, 0 <unfinished ...>
clock_nanosleep@libc.so.6(0, 0, 0x7fffb27cc460, 0x7fffb27cc460) = 0
<... nanosleep resumed> )                                    = 0
write@libc.so.6(1, "U\b", 2)                                 = 2
nanosleep@libc.so.6(0x7fffb27cc460, 0x7fffb27cc460, 2, 0 <unfinished ...>
clock_nanosleep@libc.so.6(0, 0, 0x7fffb27cc460, 0x7fffb27cc460) = 0
<... nanosleep resumed> )                                    = 0
nanosleep@libc.so.6(0x7fffb27cc460, 0x7fffb27cc460, 1, 0 <unfinished ...>
clock_nanosleep@libc.so.6(0, 0, 0x7fffb27cc460, 0x7fffb27cc460) = 0
<... nanosleep resumed> )                                    = 0
write@libc.so.6(1, "V\b", 2)                                 = 2
nanosleep@libc.so.6(0x7fffb27cc460, 0x7fffb27cc460, 2, 0 <unfinished ...>
clock_nanosleep@libc.so.6(0, 0, 0x7fffb27cc460, 0x7fffb27cc460^C <no return ...>

After process initialization, a clear(-ish) pattern emerges.
Between each call to write, a call is made to nanosleep (which in turn calls clock_nanosleep).

Presumably, removing or bypassing the calls to nanosleep will speed up the program.

We can hijack the nanosleep function from libc and replace it with our own (no-op) implementation.
One relatively simple method is to use LD_PRELOAD to tell the dynamic linker to load our library before any others.

First, we need to implement our function, which needs to match the signature of the original libc function:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// Required for the struct timespec definition.
#include <time.h>

// Override `nanosleep` from libc
int nanosleep(const struct timespec *rqtp, struct timespec *rmtp) {
    // As `man 3p nanosleep` states:
    // "If the nanosleep() function returns because the requested time has elapsed,
    //  its return value shall be zero."
    return 0;
}

Then we compile it to a position-independent shared library:

gcc -Wall -Wextra -Wno-unused-parameter -fPIC -shared -o libtimewarp.so libtimewarp.c

Now we can run the executable, using LD_PRELOAD as mentioned before:

$ LD_PRELOAD=./libtimewarp.so ./hacking-through-time
This may take a while... (ETA: ~573 days)
clubeh{0h_h0w_71m3_fl135_wh3n_y0u_h4ck_7h3_m41nfr4m3_ab40440b}

And there’s our flag, after only a few milliseconds.