Challenge
I know I have the flag somewhere in this book, but it’s over a million lines long and the awful DRM won’t let me read the file fast enough to find it remotely soon. Maybe you can help?
I spent way too long writing this problem…
By Daniel Parks (@danielp on discord)
- Solves: 22
- Points: 991/1000
- Category: Reversing
TL;DR
Sometimes brute reversing things is not the way to go, especially when they are heavily guarded - there usually are smarter ways to trick the programs, such as proxying dynamically linked libraries.
Investigation and Preliminary Patching
As with most rev challenges, it provided us a binary and no nc endpoints to connect to. So the first thing to do is of course to try running it and get a sense of how it works (who doesn’t love running suspicious code on their machines, am I right?):
$ ./slowreader
./slowreader: /lib/libc.so.6: version `GLIBC_2.34' not found (required by ./slowreader)
Oh boy, glibc incompatibility - glibc this recent will require either backporting or upgrading the entire system, which would both take a lot of work and we don’t have that much time in a CTF. Time to do some magic with the Linux dynamic linker! But first, we need to know what version of loader we should use:
$ file ./slowreader
./slowreader: ELF 32-bit LSB shared object, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, BuildID[sha1]=d31e8e5b0e045dc57571fdf52fdd0d5829182eba, for GNU/Linux 4.4.0, stripped
With that, we can just go to Ubuntu’s download page for glibc (aka libc6) and grab the deb, unpack with ar x libc6_2.34-0ubuntu3.2_amd64.deb
, then zstd -d data.tar.zst
and tar -xf data.tar
(run it in a separate directory or you will have files scattered around!). Now that we have the libraries, lets try the good ol’ LD_LIBRARY_PATH
trick:
$ LD_LIBRARY_PATH=./lib/i386-linux-gnu/ ./slowreader
Segmentation fault (core dumped)
Huh, that’s really weird - seems like the system default linker which is of the older version is incompatible with the newer glibc libraries. Let’s try again by directly calling the linker from the new glibc:
$ ./lib/ld-linux.so.2 --library-path ./lib/i386-linux-gnu/ ./slowreader
Usage: slowreader FILE
Eyy - now it works as expected. Lets pass the file provided by the challenge as a parameter:
$ ./lib/ld-linux.so.2 --library-path ./lib/i386-linux-gnu/ ./slowreader ./haystack.slowreader
Usage: slowreader FILE
Looks like it is not accepting the file for some reason. Guess it’s time to start our reversing journey:
Hmm, it’s checking for the slowreader
string, but we have to pass a valid path to the loader. Might as well just patch the check out entirely by bypassing the jnz
- with the patch at 1CD7A
0F 85 C2 00 00 00
-> 90 90 90 90 90 90
, we can try running it again now:
$ ./lib/ld-linux.so.2 --library-path ./lib/i386-linux-gnu/ ./slowreader ./haystack.slowreader
Welcome to SlowReader(c) 6.96.9
You are allowed to read 1 line every 2 seconds
Loading file...
Press ENTER to load a line.
Nice! It seems to want us to press enter over and over again to advance line by line, but we can do better:
Just a simple getline
call, nothing that can’t be patched out. By patching the loop end jmp
to skip the check with 1CE41
C0
-> DD
, we can press enter once and it will loop indefinitely:
$ ./lib/ld-linux.so.2 --library-path ./lib/i386-linux-gnu/ ./slowreader ./haystack.slowreader
Welcome to SlowReader(c) 6.96.9
You are allowed to read 1 line every 2 seconds
Loading file...
Press ENTER to load a line.
This is not the needle you are looking for (move along).
This is not the needle you are looking for (move along).
(...)
Great! But now the main problem comes - the lines are still only being read every 2 seconds. How are we gonna deal with it?
Wait, how’d it do that?
The 2 second delay has 2 possibilities - either they used a crude execution loop to time the delay, or they called one of the time related Linux functions to determine their elapsed time. The former seems very unlikely - the delay is very accurate, and the timer knows to delay if we press too fast but allows instantly if we press enter later than the delay, which would require that delay timer to be running in another thread if it was an execution loop. Looking at the imported functions in extern
, we can prove that it is indeed the latter:
Function name Segment Start Length Locals Arguments R F L M S B T =
fileno extern 0002F4B8 00000004 00000000 00000000 R . . . . . T .
__errno_location extern 0002F4BC 00000004 00000000 00000000 R . . . . . T .
strerror extern 0002F4C0 00000004 00000000 00000000 R . . . . . T .
clock_gettime extern 0002F4C4 00000004 00000000 00000000 R . . . . . T .
ptrace extern 0002F4C8 00000004 00000000 00000000 R . . . . . T .
fgets extern 0002F4CC 00000004 00000000 00000000 R . . . . . T .
EVP_DecryptInit_ex extern 0002F4D0 00000004 R . . . . . . .
perror extern 0002F4D4 00000004 00000000 00000000 R . . . . . T .
free extern 0002F4D8 00000004 00000000 00000000 R . . . . . T .
getline extern 0002F4DC 00000004 R . . . . . . .
fclose extern 0002F4E0 00000004 00000000 00000000 R . . . . . T .
fopen extern 0002F4E4 00000004 00000000 00000000 R . . . . . T .
getppid extern 0002F4E8 00000004 00000000 00000000 R . . . . . T .
strcpy extern 0002F4EC 00000004 00000000 00000000 R . . . . . T .
printf extern 0002F4F0 00000004 00000000 00000000 R . . . . . T .
EVP_CIPHER_CTX_new extern 0002F4F4 00000004 R . . . . . . .
strstr extern 0002F4F8 00000004 00000000 00000000 R . . . . . T .
EVP_aes_256_cbc extern 0002F4FC 00000004 R . . . . . . .
malloc extern 0002F500 00000004 00000000 00000000 R . . . . . T .
__stack_chk_fail extern 0002F504 00000004 . . . . . . . .
memmove extern 0002F508 00000004 00000000 00000000 R . . . . . T .
puts extern 0002F50C 00000004 00000000 00000000 R . . . . . T .
rand extern 0002F510 00000004 00000000 00000000 R . . . . . T .
EVP_CIPHER_CTX_set_paddingextern 0002F514 00000004 R . . . . . . .
index extern 0002F518 00000004 00000000 00000000 R . . . . . T .
fread extern 0002F51C 00000004 00000000 00000000 R . . . . . T .
snprintf extern 0002F520 00000004 00000000 00000000 R . . . . . T .
kill extern 0002F524 00000004 00000000 00000000 R . . . . . T .
__libc_start_main extern 0002F528 00000004 00000000 00000000 R . . . . . T .
EVP_DecryptUpdate extern 0002F52C 00000004 R . . . . . . .
strcmp extern 0002F530 00000004 00000000 00000000 R . . . . . T .
__cxa_finalize extern 0002F534 00000004 00000000 00000000 R . . . . . T .
exit extern 0002F538 00000004 00000000 00000000 . . . . . . T .
clock_gettime
is referenced, whereas any threading calls are missing. But how are they referenced? Surely they didn’t just make a simple comparison that is easily patched out:
Yikes - reversing this directly will be a pain for sure. After poking around for quite a bit to see if there is any obvious places for time comparison (taking 2 tp->tv_sec
at 2 different times and comparing, for example) and patching random bits and pieces, there still wasn’t any good hint of how it’s called. Tackling it in another direction seems to be time better spent than figuring out how this obfuscation works.
Since clock_gettime
eventually enters kernel space through a syscall called, you guessed it, clock_gettime
, strace
should be able to pick it up, and we can figure out where and how the calls are made using the address information strace provides. Time to fire it up:
$ strace ./lib/ld-linux.so.2 --library-path ./lib/i386-linux-gnu/ ./slowreader ./haystack.slowreader
(...)
ptrace(PTRACE_TRACEME) = -1 EPERM (Operation not permitted)
getppid() = 19426
kill(19426, SIGTERM) = 0
strace: Process 19429 detached
Terminated
Seems like they actually implemented a typical anti-debug measure using ptrace(PTRACE_TRACEME)
calls - this call fails with EPERM if there is a ptrace parent attached, so the code flow can be redirected, which for this case is into termination. No worries - we can just patch it out if it’s simple. Tracing xrefs from the ptrace function, we can see where it’s utilized:
Yep, straightforward enough - skipping the jz
check with 1CF26
74 12
-> 90 90
should do the job.
$ strace ./lib/ld-linux.so.2 --library-path ./lib/i386-linux-gnu/ ./slowreader ./haystack.slowreader
(...)
read(4, "Name:\tstrace\nUmask:\t0077\nState:\t"..., 1024) = 1024
close(4) = 0
kill(20063, SIGBUSBus error (core dumped)
$ Press ENTER to load a line.
What? strace got terminated by SIGBUS
while the child process is still going (it detaches from the terminal and goes into background), which really shouldn’t be something that the patch can introduce. After a lot of poking around with ptrace returns and such, it ended up being a problem completely unrelated to the patch, but once again glibc itself - it seems like both the parent and the child have to be using the same glibc version. However, we have a slight problem - strace is 64bit, which means we have to fetch the 64bit version of glibc again using the same method, and then use 2 loaders to load each one in respectively.
$ ./lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 --library-path ./lib/x86_64-linux-gnu/ /usr/bin/strace -fi ./lib/ld-linux.so.2 --library-path ./lib/i386-linux-gnu/ ./slowreader ./haystack.slowreader
[f7f32149] getppid() = 24763
[f7f32149] openat(AT_FDCWD, "/proc/24763/cmdline", O_RDONLY) = 4
[f7f32149] statx(4, "", AT_STATX_SYNC_AS_STAT|AT_NO_AUTOMOUNT|AT_EMPTY_PATH, STATX_BASIC_STATS, {stx_mask=STATX_BASIC_STATS, stx_attributes=0, stx_mode=S_IFREG|0444, stx_size=0, ...}) = 0
[f7f32149] read(4, "./lib/x86_64-linux-gnu/"..., 1024) = 247
[f7f32149] close(4) = 0
[f7f32149] write(1, "This is not the needle you are l"..., 57This is not the needle you are looking for (move along).
) = 57
With an overly long command, we can finally strace reliably - but where did the clock_gettime
calls go? -f
should’ve handled threading, and the program ain’t calling any threading functions to begin with. Upon further reading, it seems that clock_gettime
can now be using vDSO instead of syscalls, which strace cannot log. Guess we are at a dead end - time to find another path in.
Speeding up
Just as I was stuck figuring out what to do next, @Kevin gave a great idea: Instead of reversing and patching the clock_gettime
calls, is there any way to emulate time to speed it up? I know RDTSC emulation and syscall emulation is possible, but writing it ourselves would take quite a bit of time, especially when my only experience emulating RDTSC is for windows and it requires a lot of OS specific internals knowledge.
Apparently though, @Kevin noticed one such library that does exactly what we want exists, and it’s called libfaketime - which can be loaded using LD_PRELOAD
, but since we are manually invoking loaders, we can just use the --preload
option instead. After going through the syntax and wondering why it doesnt work even with known working commands like watch
and date
, we eventually figured out FAKETIME
requires a date modifier then a speed modifier. Time to test it out:
$ FAKETIME='@2000-01-01 11:12:13 x20000' ./lib/ld-linux.so.2 --library-path ./lib/i386-linux-gnu/ --pre
load "./usr/lib/i386-linux-gnu/faketime/libfaketime.so.1" ./slowreader ./haystack.slowreader
Welcome to SlowReader(c) 6.96.9
You are allowed to read 1 line every 2 seconds
Loading file...
Press ENTER to load a line.
This is not the needle you are looking for (move along).
This is not the needle you are looking for (move along).
(...)
Woah now that’s speed - it is practically running at real time, as if the 2 second restriction was never placed in the first place. All that’s left is to wait for it to print all the lines, and then fish out the flag, right?
(...)
This is not the needle you are looking for (move along).
This is not the needle you are looking for (move along).
This is not the needle you are looking for (move along).
[4]+ Stopped FAKETIME='@2000-01-01 11:12:13 x20000' ./lib/ld-linux.so.2 --library-path ./lib/i386-linux-gnu/ --preload "./usr/lib/i386-linux-gnu/faketime/libfaketime.so.1" ./slowreader ./haystack.slowreader
Oh no - this is a really bad sign that something broke. Under strace, we can get some more info on it:
$ FAKETIME='@2000-01-01 11:12:13 x20000' ./lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 --library-path ./lib/x86_64-linux-gnu/ /usr/bin/strace -fi ./lib/ld-linux.so.2 --library-path ./lib/i386-linux-gnu/ --preload "./usr/lib/i386-linux-gnu/faketime/libfaketime.so.1" ./slowreader ./haystack.slowreader
(...)
[f7f1d149] read(6, "Name:\tld-linux-x86-64\nUmask:\t007"..., 1024) = 1024
[f7f1d149] close(6) = 0
[f7ef6058] --- SIGSEGV {si_signo=SIGSEGV, si_code=SEGV_MAPERR, si_addr=NULL} ---
[????????] +++ killed by SIGSEGV (core dumped) +++
Segmentation fault (core dumped)
Oh boy - something is not right here. Digging into the core dump generated by apport-unpack
on the file in /var/crash/
(on a Ubuntu system) using gdb
, we get this:
Program terminated with signal SIGSEGV, Segmentation fault.
#0 0xf7eaa058 in ?? ()
(gdb) bt
#0 0xf7eaa058 in ?? ()
#1 0xf7eb8e28 in ?? ()
#2 0xf79ae4ca in ?? ()
Backtrace stopped: previous frame inner to this frame (corrupt stack?)
(gdb) x/10i 0xf7eaa058
=> 0xf7eaa058: mov %eax,(%esi)
0xf7eaa05a: mov 0xf7ecb2c8,%edi
0xf7eaa060: mov 0xf7ecb330,%esi
0xf7eaa066: call 0xf7eb5ab0
0xf7eaa06b: imul $0x3587719a,-0x81442e8(,%eax,4),%ebx
0xf7eaa076: call 0xf7eb5ad0
0xf7eaa07b: mov -0x81442dc(,%eax,4),%eax
0xf7eaa082: add %ebx,%eax
0xf7eaa084: add $0x8773f3a0,%eax
0xf7eaa089: cmp %eax,%esi
(gdb) x/100bx 0xf7eaa058
0xf7eaa058: 0x89 0x06 0x8b 0x3d 0xc8 0xb2 0xec 0xf7
0xf7eaa060: 0x8b 0x35 0x30 0xb3 0xec 0xf7 0xe8 0x45
0xf7eaa068: 0xba 0x00 0x00 0x69 0x1c 0x85 0x18 0xbd
0xf7eaa070: 0xeb 0xf7 0x9a 0x71 0x87 0x35 0xe8 0x55
0xf7eaa078: 0xba 0x00 0x00 0x8b 0x04 0x85 0x24 0xbd
0xf7eaa080: 0xeb 0xf7 0x01 0xd8 0x05 0xa0 0xf3 0x73
0xf7eaa088: 0x87 0x39 0xc6 0x0f 0x9c 0xc3 0xe8 0x5d
0xf7eaa090: 0xba 0x00 0x00 0x8b 0x04 0x85 0x30 0xbd
0xf7eaa098: 0xeb 0xf7 0xb9 0x7e 0x0f 0xdd 0xde 0x31
0xf7eaa0a0: 0xc8 0x8d 0x34 0x07 0x83 0xc6 0x01 0x0f
0xf7eaa0a8: 0xaf 0xf7 0xe8 0x61 0xba 0x00 0x00 0x0f
0xf7eaa0b0: 0xb6 0x0c 0x85 0x3c 0xbd 0xeb 0xf7 0xbf
0xf7eaa0b8: 0x78 0x9b 0x05 0x05
(gdb)
Which after searching for bytes in IDA, maps to address E058
before ASLR.
Seems like the program is trying to dereference the return value of fileno
for some reason, which is not a pointer but an integer representing the file descriptor - why would they do that?
Understanding how this happens is probably as difficult as reversing how clock_gettime
works itself, not to mention how sporadic this failure is even with slow time multipliers - which means it is time to move onto yet another method to try.
Proxying
Thinking back to the days where I used to bypass kernel protection using dll proxying on windows, I suddenly wondered if it is possible on Linux too, in order to grab information directly from the openssl
EVP_Decrypt*
functions. This way, we can at least know if the key and iv dynamically changes, and how the program decrypts the haystack file. Turns out, LD_PRELOAD
allows exactly this with much more ease than windows - it overrides any function of any library with the .so
s provided to it. Since we mainly want to get the key and iv, we can choose EVP_DecryptInit_ex
to proxy first, of which the function signature is freely available on man page. With that knowledge, we can write something similar to this:
#include <stdio.h>
#include <string.h>
int EVP_DecryptInit_ex(void *ctx, const void *type, void *impl, const unsigned char *key, const unsigned char *iv) {
int i;
printf("key 0x");
for (i=0; i < 32; i++)
printf("%02x",key[i] & 0xff);
printf("\niv 0x");
for (i=0; i < 16; i++)
printf("%02x",iv[i] & 0xff);
printf("\n");
return 0;
}
Note that the specific struct pointers are all changed to void*
to avoid needing to link openssl libraries, since we don’t need them anyways. The key and iv sizes are fixed - AES-256-CBC requires 32 bytes for key and 16 bytes for iv respectively. Compiling with gcc -fPIC -shared -m32 evpintercept.c -o evpintercept.so
and preloading it gets us:
$ ./lib/ld-linux.so.2 --library-path ./lib/i386-linux-gnu/ --preload ./evpintercept.so ./slowreader ./h
aystack.slowreader
Welcome to SlowReader(c) 6.96.9
You are allowed to read 1 line every 2 seconds
Loading file...
key 0x4498a9650fa72cf38d0777a611b33c140172cfef7bda174fba255c4a59551678
iv 0x3d07501c1386909a3eee28e8e97a5e29
[2]+ Stopped ./lib/ld-linux.so.2 --library-path ./lib/i386-linux-gnu/ --preload ./evpintercept.so ./slowreader ./haystack.slowreader
Nice, we got the first key! However, since we are writing a stub that prints and returns without doing the actual decrypt init routines, it segfaults right afterwards as expected. We would like to see whether there are more keys and ivs that are being used after this initial load - the file might be segmented into chunks of data with different keys that the program computes and cycles through. This is where dynamic linking comes into play - we can utilize the dlopen
and dlsym
functions, which are analogous to LoadLibraryEx
and GetProcAddress
for Windows, to get a function pointer to call on.
#include <stdio.h>
#include <string.h>
#include <dlfcn.h>
int EVP_DecryptInit_ex(void *ctx, const void *type, void *impl, const unsigned char *key, const unsigned char *iv) {
int i;
printf("key 0x");
for (i=0; i < 32; i++)
printf("%02x",key[i] & 0xff);
printf("\niv 0x");
for (i=0; i < 16; i++)
printf("%02x",iv[i] & 0xff);
printf("\n");
void *handle = dlopen("/usr/lib/libssl.so.1.1", RTLD_NOW);
int (*orig_func)() = dlsym(handle, "EVP_DecryptInit_ex");
return orig_func(ctx, type, impl, key, iv);
}
Compiling and preloading again gives us the following output:
$ ./lib/ld-linux.so.2 --library-path ./lib/i386-linux-gnu/ --preload ./evpintercept.so ./slowreader ./haystack.slowreader
Welcome to SlowReader(c) 6.96.9
You are allowed to read 1 line every 2 seconds
Loading file...
key 0x4498a9650fa72cf38d0777a611b33c140172cfef7bda174fba255c4a59551678
iv 0x3d07501c1386909a3eee28e8e97a5e29
Press ENTER to load a line.
This is not the needle you are looking for (move along).
This is not the needle you are looking for (move along).
(...)
Nice! It’s not segfaulting anymore, and it doesn’t seem like the key and iv is ever changed. Just to be sure that the keys are for decrypting the haystack file, we can proxy EVP_DecryptUpdate
to check what the keys are being used for:
int EVP_DecryptUpdate(void *ctx, unsigned char *out,int *outl, unsigned char *in, int inl) {
int i;
printf("ciphertext of length 0x%d: 0x", inl);
for (i=0; i < inl; i++)
printf("%02x",in[i] & 0xff);
printf("\n");
void *handle = dlopen("/usr/lib/libssl.so.1.1", RTLD_NOW);
int (*orig_func)() = dlsym(handle, "EVP_DecryptUpdate");
int ret = orig_func(ctx, out, outl, in, inl);
printf("plaintext of length 0x%d: %s\n\n", *outl, out);
return ret;
}
Time to check what prints after we press enter:
Press ENTER to load a line.
ciphertext of length 0x16: 0xf97f2a875aa1034513dc2ebbfb3d2539
plaintext of length 0x16: This is not the
ciphertext of length 0x16: 0x318d88be24200e1dac9facf80e13e67c
plaintext of length 0x16: needle you are l
ciphertext of length 0x16: 0x4b8bebb6bb4c5cd04e7ae499c610c05f
plaintext of length 0x16: ooking for (move
ciphertext of length 0x16: 0xc66a76feb2852f003ea35346fc62ca9c
plaintext of length 0x16: along).
This is
This is not the needle you are looking for (move along).
Exactly what we are looking for! Time to try the keys out and hope for the best.
Finding the needle in the haystack
I’ve always had a tendency to screw up code for encryption and decryption for some reason, but @Kevin is once again back to save my bacon, and he quickly whipped up a decrypter:
from Crypto.Cipher import AES
key = bytes.fromhex("4498a9650fa72cf38d0777a611b33c140172cfef7bda174fba255c4a59551678")
iv = bytes.fromhex("3d07501c1386909a3eee28e8e97a5e29")
cipher = AES.new(key, AES.MODE_CBC)
f = open("haystack.slowreader", "rb")
ct = f.read()
pt = cipher.decrypt(ct)
g = open("flag.txt", "wb")
g.write(pt)
Looking at the file, we see some junk in the header and footer, but:
slowreader book
This is not the needle you are looking for (move along).
This is not the needle you are looking for (move along).
This is not the needle you are looking for (move along).
It worked! All that’s left is to actually find the flag in the textstacks - something we can easily do with a simple ctrl-f for utflag
. Thus finally we have the flag:
utflag{ghidra_isnt_always_the_answer}
at line 1789678
!