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:
- Tracing library calls might help.
- This ain’t
exploit
: nothing’s stopping you from hooking whatever you want. - The
hacking-through-time-arm64
file is for ARM64 computers, such as newer Apple devices.
Most players will want thehacking-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:
|
|
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.