Pages

Categories

DFIR – CTF – LootStash

The Hack The Box CTF Challenge ‘LootStash’ is another software reverse engineering task. Like before, the objective is to obtain the flag!

Challenge: LootStash

The LootStash challenge presents it self as a ‘giant weapons cache’ which you are able to open and obtain a item. However, there’s only one item you’re interested in, can you obtain it.

Downloads:

The CTF provides the following scenario files:

Filename:MD5:
rev_lootstash.zip05F55756CC322F84715A602BED14E51C
rev_lootstash.zip\stash4FE90D112BDCBCC1966D6259F739635B

Our interest is in the ‘stash

Static Analysis

There’s no file extension, so lets look at the header file.

PowerShell:
((Get-Content .\stash -Encoding Byte -TotalCount 50) | ForEach-Object { [char] $_ }) -join ""
ELF>À @0p

So we have a UNIX executable in ELF format. Lets see what else we can figure out using Detect It Easy:

Detect It Easy v3.10 – .\stash
ELF64
Operation system: Debian Linux(ABI: 3.2.0)[AMD64, 64-bit, DYN]
Compiler:         GCC((Debian 12.2.0-14) 12.2.0)
Language:         C
Library:          GLIBC(2.34)[DYN AMD64-64]
Overlay:          Binary[Offset=0x7000,Size=0x07f0]

Main focus points:

  • It’s a Unix ELF executable.
  • Programming language used: C
  • Compiler: GCC 12.2.0
  • Library: GLIBC v2.34

Locating and extracting strings also gives us some insight:

Strings [Truncated]:
Number  Offset  String
0       0318        /lib64/ld-linux-x86-64.so.2
1       0373        V1YahD
2       0531        setvbuf
3       0539        sleep
4       0544        putchar
5       0551        stdout
6       0558        __libc_start_main
7       056a        srand
8       0570        __cxa_finalize
9       057f        printf
10      0586        libc.so.6
11      0590        GLIBC_2.34
12      059b        GLIBC_2.2.5
13      05a7        _ITM_deregisterTMCloneTable
14      05c3        __gmon_start__
15      05d2        _ITM_registerTMCloneTable
16      3008        Ebony, Core of Perdition
17      3028        Phantomdream, Trinket of the Corrupted
18      304f        Earthsong, Dawn of Visions
19      3070        Torment, Beacon of Twilight's End
20      3092        Moonshard, Baton of the Wind
21      30af        Mirage, Bead of Secrets
22      30c7        Inertia, Sphere of Nightmares
23      30e5        Spellkeeper, Glory of Pride
24      3101        Mirage, Stone of the Wind
25      311b        Ghost, Baton of Suffering
26      3135        Scar, Idol of Horrors
27      3150        Phantomsong, Focus of Fallen Souls
28      3173        Netherlight, Touch of Woe
29      318d        Scarlet, Visage of Summoning
...

The list of loot items are in plain text here, along with other display messages which we have yet to see. There are 256 items.

Another area to look at is the symbols table which holds global/static variable names, labels, and functions calls within the application. We can use it to see what to expect from the program’s execution.

Symbol Table:
#   Name
0
1   putcha
2   __libc_start_main
3   _ITM_deregisterTMCloneTable
4   puts
5   printf
6   srand
7   __gmon_start__
8   time
9   setvbuf
10  _ITM_registerTMCloneTable
11  sleep
12  rand
13  stdout
14  __cxa_finalize

Some of the above symbols of interest:

  • putchar
  • puts
  • printf
  • srand
  • time
  • setvbuf
  • sleep
  • rand

We can expect some random seeding and number generation shenanigans from this challenge… and something to do with the current time?

Now let’s process it in Ghidra and see what we can decipher.

undefined8 main(void)
{
  int iVar1;
  time_t tVar2;
  int local_c;
  
  setvbuf(stdout,(char *)0x0,2,0);
  tVar2 = time((time_t *)0x0);
  srand((uint)tVar2);
  puts("Diving into the stash - let\'s see what we can find.");
  for (local_c = 0; local_c < 5; local_c = local_c + 1) {
    putchar(0x2e);
    sleep(1);
  }
  iVar1 = rand();
  printf("\nYou got: \'%s\'. Now run, before anyone tries to steal it!\n",
         *(undefined8 *)(gear + (long)(int)((ulong)(long)iVar1 % 0x7f8 >> 3) * 8));
  return 0;
}

and now let’s clean it up a bit…

int main()
{
    setvbuf(stdout, (char *)0x0, 2, 0);

    time_t current_time;
    current_time = time((time_t *)0x0);
    srand((uint)current_time);
    
    puts("Diving into the stash - let\'s see what we can find.");
    
    for (int wait_counter = 0; wait_counter < 5; wait_counter++)
    {
        putchar('.');
        sleep(1);
    }
    
    int gear_index;
    gear_index = rand();

    printf("\nYou got: \'%s\'. Now run, before anyone tries to steal it!\n",
          *(int *)(gear + (long)(int)((ulong)(long)gear_index % 2040 >> 3) * 8));
    
    return 0;
}

It uses the current time as a seed for the ‘rand‘ function, and the random number generated is used as an index to the ‘gear‘ array. While reviewing decompiled code it’s important we know exactly what is going on, so here’s a recap of the functions used here from the libc manual, specifically for libc v2.34.

Function/Data Type:Description:
time_tData Type: time_t time_t is the simplest data type used to represent simple calendar time. In ISO C, time_t can be either an integer or a floating-point type, and the meaning of time_t values is not specified. The only things a strictly conforming program can do with time_t values are: pass them to difftime to get the elapsed time between two simple calendar times (see Calculating Elapsed Time), and pass them to the functions that convert them to broken-down time (see Broken-down Time). On POSIX-conformant systems, time_t is an integer type and its values represent the number of seconds elapsed since the epoch, which is 00:00:00 on January 1, 1970, Coordinated Universal Time. The GNU C Library additionally guarantees that time_t is a signed type, and that all of its functions operate correctly on negative time_t values, which are interpreted as times before the epoch.
setvbufFunction: int setvbuf (FILE *stream, char *buf, int mode, size_t size) Preliminary: | MT-Safe | AS-Unsafe corrupt | AC-Unsafe lock corrupt | See POSIX Safety Concepts. This function is used to specify that the stream stream should have the buffering mode mode, which can be either _IOFBF (for full buffering), _IOLBF (for line buffering), or _IONBF (for unbuffered input/output). If you specify a null pointer as the buf argument, then setvbuf allocates a buffer itself using malloc. This buffer will be freed when you close the stream. Otherwise, buf should be a character array that can hold at least size characters. You should not free the space for this array as long as the stream remains open and this array remains its buffer. You should usually either allocate it statically, or malloc (see Unconstrained Allocation) the buffer. Using an automatic array is not a good idea unless you close the file before exiting the block that declares the array. While the array remains a stream buffer, the stream I/O functions will use the buffer for their internal purposes. You shouldn’t try to access the values in the array directly while the stream is using it for buffering. The setvbuf function returns zero on success, or a nonzero value if the value of mode is not valid or if the request could not be honored.   Macro: int _IOFBF The value of this macro is an integer constant expression that can be used as the mode argument to the setvbuf function to specify that the stream should be fully buffered.   Macro: int _IOLBF The value of this macro is an integer constant expression that can be used as the mode argument to the setvbuf function to specify that the stream should be line buffered.   Macro: int _IONBF The value of this macro is an integer constant expression that can be used as the mode argument to the setvbuf function to specify that the stream should be unbuffered.
timeFunction: time_t time (time_t *result) Preliminary: | MT-Safe | AS-Safe | AC-Safe | See POSIX Safety Concepts. This is the simplest function for getting the current calendar time. It returns the calendar time as a value of type time_t; on POSIX systems, that means it has a resolution of one second. It uses the same clock as ‘clock_gettime (CLOCK_REALTIME_COARSE)’, when the clock is available or ‘clock_gettime (CLOCK_REALTIME)’ otherwise. If the argument result is not a null pointer, the calendar time value is also stored in *result. This function cannot fail.
srandFunction: void srand (unsigned int seed) Preliminary: | MT-Safe | AS-Unsafe lock | AC-Unsafe lock | See POSIX Safety Concepts. This function establishes seed as the seed for a new series of pseudo-random numbers. If you call rand before a seed has been established with srand, it uses the value 1 as a default seed. To produce a different pseudo-random series each time your program is run, do srand (time (0)).
putsFunction: int puts (const char *s) Preliminary: | MT-Safe | AS-Unsafe corrupt | AC-Unsafe lock corrupt | See POSIX Safety Concepts. The puts function writes the string s to the stream stdout followed by a newline. The terminating null character of the string is not written. (Note that fputs does not write a newline as this function does.) puts is the most convenient function for printing simple messages. For example: puts (“This is a message.”); outputs the text ‘This is a message.’ followed by a newline.
putcharFunction: int putchar (int c) Preliminary: | MT-Safe | AS-Unsafe corrupt | AC-Unsafe corrupt lock | See POSIX Safety Concepts. The putchar function is equivalent to putc with stdout as the value of the stream argument.
sleepFunction: unsigned int sleep (unsigned int seconds) Preliminary: | MT-Unsafe sig:SIGCHLD/linux | AS-Unsafe | AC-Unsafe | See POSIX Safety Concepts. The sleep function waits for seconds or until a signal is delivered, whichever happens first. If sleep function returns because the requested interval is over, it returns a value of zero. If it returns because of delivery of a signal, its return value is the remaining time in the sleep interval. The sleep function is declared in unistd.h.
randFunction: int rand (void) Preliminary: | MT-Safe | AS-Unsafe lock | AC-Unsafe lock | See POSIX Safety Concepts. The rand function returns the next pseudo-random number in the series. The value ranges from 0 to RAND_MAX.   Macro: int RAND_MAX The value of this macro is an integer constant representing the largest value the rand function can return. In the GNU C Library, it is 2147483647, which is the largest signed integer representable in 32 bits. In other libraries, it may be as low as 32767.
printfFunction: int printf (const char *template, …) Preliminary: | MT-Safe locale | AS-Unsafe corrupt heap | AC-Unsafe mem lock corrupt | See POSIX Safety Concepts. The printf function prints the optional arguments under the control of the template string template to the stream stdout. It returns the number of characters printed, or a negative value if there was an output error.

Reference links for the above:

Sandbox Analysis:

So we have a basic idea of what to expect, let’s run it in a sandboxed environment.

A Linux Ubuntu 24.04.2 LTS virtual machine is setup, the stash file transferred across, and now let’s execute it within a terminal window.

The `date` command was run immediately before running stash so we can see as time changes, a new loot item is given.

jamie@VM-Linux-Ubuntu:~/Documents$ date --iso-8601='seconds'; ./stash
2025-08-02T20:22:38+00:00
Diving into the stash - let's see what we can find.
.....
You got: 'Nexus, Jewel of Pride's Fall'. Now run, before anyone tries to steal it!
jamie@VM-Linux-Ubuntu:~/Documents$ date --iso-8601='seconds'; ./stash
2025-08-02T20:22:44+00:00
Diving into the stash - let's see what we can find.
.....
You got: 'Prophecy, Knapsack of Blessings'. Now run, before anyone tries to steal it!
jamie@VM-Linux-Ubuntu:~/Documents$ date --iso-8601='seconds'; ./stash
2025-08-02T20:22:51+00:00
Diving into the stash - let's see what we can find.
.....
You got: 'Purgatory, Insignia of the Light'. Now run, before anyone tries to steal it!
jamie@VM-Linux-Ubuntu:~/Documents$ date --iso-8601='seconds'; ./stash
2025-08-02T20:22:58+00:00
Diving into the stash - let's see what we can find.
.....
You got: 'Deathsong, Guardian of Redemption'. Now run, before anyone tries to steal it!

And as expected, every execution returns a different value. The ‘.....‘ is where an artificial delay of 5 seconds is held before the loot is displayed.

Now let’s fake the current time and brute force it! We’ll change the system clock and run the application, and repeat it until we get the answer we’re looking for.

… just kidding.

Linux has a useful command called faketime. Pass it a time of your choice along with another command/executable, and all date/time specific requests are intercepted and swapped with the fake one. Here’s an example which prints the current time, and the same command with the faketime interceptor with an increment of 1 second each time from the UNIX epoch:

jamie@VM-Linux-Ubuntu:~/Documents$ date --iso-8601='seconds'; faketime '1970-01-01T00:00:00' date --iso-8601='seconds'
2025-08-02T20:40:07+00:00
1970-01-01T00:00:00+00:00
jamie@VM-Linux-Ubuntu:~/Documents$ date --iso-8601='seconds'; faketime '1970-01-01T00:00:01' date --iso-8601='seconds'
2025-08-02T20:40:11+00:00
1970-01-01T00:00:01+00:00
jamie@VM-Linux-Ubuntu:~/Documents$ date --iso-8601='seconds'; faketime '1970-01-01T00:00:02' date --iso-8601='seconds'
2025-08-02T20:40:17+00:00
1970-01-01T00:00:02+00:00

We can use this same faketime command to manipulate what loot we receive from the stash. Here’s proof we can keep receiving the same item:

jamie@VM-Linux-Ubuntu:~/Documents$ date --iso-8601='seconds'; faketime '1970-01-01T00:00:01' ./stash
2025-08-02T20:54:33+00:00
Diving into the stash - let's see what we can find.
.....
You got: 'Dreamkiss, Might of Traitors'. Now run, before anyone tries to steal it!
jamie@VM-Linux-Ubuntu:~/Documents$ date --iso-8601='seconds'; faketime '1970-01-01T00:00:01' ./stash
2025-08-02T20:54:41+00:00
Diving into the stash - let's see what we can find.
.....
You got: 'Dreamkiss, Might of Traitors'. Now run, before anyone tries to steal it!
jamie@VM-Linux-Ubuntu:~/Documents$ date --iso-8601='seconds'; faketime '1970-01-01T00:00:01' ./stash
2025-08-02T20:54:48+00:00
Diving into the stash - let's see what we can find.
.....
You got: 'Dreamkiss, Might of Traitors'. Now run, before anyone tries to steal it!

And we can increment the fake time by one second to start brute forcing the answer:

jamie@VM-Linux-Ubuntu:~/Documents$ date --iso-8601='seconds'; faketime '1970-01-01T00:00:01' ./stash
2025-08-02T20:56:05+00:00
Diving into the stash - let's see what we can find.
.....
You got: 'Dreamkiss, Might of Traitors'. Now run, before anyone tries to steal it!
jamie@VM-Linux-Ubuntu:~/Documents$ date --iso-8601='seconds'; faketime '1970-01-01T00:00:02' ./stash
2025-08-02T20:56:13+00:00
Diving into the stash - let's see what we can find.
.....
You got: 'Aqua, Destroyer of the Ancients'. Now run, before anyone tries to steal it!
jamie@VM-Linux-Ubuntu:~/Documents$ date --iso-8601='seconds'; faketime '1970-01-01T00:00:03' ./stash
2025-08-02T20:56:26+00:00
Diving into the stash - let's see what we can find.
.....
You got: 'Lightbane, Crusader of Trembling Hands'. Now run, before anyone tries to steal it!

The artificial 5 second delay is going to stop any serious attempt. With 255 loot items, this would take 21 minutes and 15 seconds calling them back to back to get all possible loot item outcomes. That is the best case scenario if the returned random number with the manipulated seed are calculated to get a unique item each time… not going to happen. We would have to know in advanced what date/time is required to seed the random number generator to return consecutive integers from 0-255.

The brute force could be batched in threads to run concurrently, dividing the time taken to brute force, but this is still not ideal, and there would still be a 5 second delay. If we came across a similar problem with a longer delay it would further hinder any brute force attempt.

Patching!

What if we just take out the sleep by modifying and patching the assembly instructions? We can change the argument to the sleep call from a 5 to a 0; by far the easiest. Or go a tip-toe further and just patch out the sleep function call with a no operation (NOP-90).

Here’s both:

  • Sleep Argument Patching:
    1. Identify the sleep function call.
    2. Locate the argument being pushed onto the stack prior to being called.
    3. Confirm this is the call to sleep.
    4. Modify the argument value from 0x5 to 0x0 (0 seconds) – little endian 4 byte (32 bit) integer.
    5. Confirm the amended changes in the decompilation window.
  • Sleep Operation Patching:
    1. Identifying the sleep function call.
    2. Locate the assembly instructions for the respective arguments and function call.
    3. Modify them to be a no operation call (NOP-90).
    4. Confirm the amended changes in the decompilation window.

After patching, export the binary and execute them to confirm its success. Here’s the output of executing the patched executable with a pre-command of date to show there is no artificial blocking by way of the sleep.

Here’s stash patched with Sleep 0:

jamie@VM-Linux-Ubuntu:~/Documents$ date --iso-8601='seconds'; faketime '1970-01-01T00:00:01' ./stashPatchSleep0 
2025-08-03T00:09:54+00:00
Diving into the stash - let's see what we can find.
.....
You got: 'Dreamkiss, Might of Traitors'. Now run, before anyone tries to steal it!
jamie@VM-Linux-Ubuntu:~/Documents$ date --iso-8601='seconds'; faketime '1970-01-01T00:00:02' ./stashPatchSleep0 
2025-08-03T00:09:56+00:00
Diving into the stash - let's see what we can find.
.....
You got: 'Aqua, Destroyer of the Ancients'. Now run, before anyone tries to steal it!
jamie@VM-Linux-Ubuntu:~/Documents$ date --iso-8601='seconds'; faketime '1970-01-01T00:00:03' ./stashPatchSleep0 
2025-08-03T00:09:59+00:00
Diving into the stash - let's see what we can find.
.....
You got: 'Lightbane, Crusader of Trembling Hands'. Now run, before anyone tries to steal it!

Here’s stash patched with NOP:

jamie@VM-Linux-Ubuntu:~/Documents$ date --iso-8601='seconds'; faketime '1970-01-01T00:00:01' ./stashPatchNOP 
2025-08-03T00:08:29+00:00
Diving into the stash - let's see what we can find.
.....
You got: 'Dreamkiss, Might of Traitors'. Now run, before anyone tries to steal it!
jamie@VM-Linux-Ubuntu:~/Documents$ date --iso-8601='seconds'; faketime '1970-01-01T00:00:02' ./stashPatchNOP 
2025-08-03T00:08:32+00:00
Diving into the stash - let's see what we can find.
.....
You got: 'Aqua, Destroyer of the Ancients'. Now run, before anyone tries to steal it!
jamie@VM-Linux-Ubuntu:~/Documents$ date --iso-8601='seconds'; faketime '1970-01-01T00:00:03' ./stashPatchNOP 
2025-08-03T00:08:34+00:00
Diving into the stash - let's see what we can find.
.....
You got: 'Lightbane, Crusader of Trembling Hands'. Now run, before anyone tries to steal it!

Both methods produce the same result. But we’ll go with the no operation approach as it’s technically faster.

Solution – TimeForLoot.py

At this point, we have an executable which runs and returns as fast as possible, and we can intercept the current date calls and replace it with our own. Now let’s make a python script to brute force an answer for all possible loot items:

import subprocess
import datetime

loot = {}
unix_seconds_attempt = 0

while len(loot) < 255:
    attempted_datetime = datetime.datetime.fromtimestamp(unix_seconds_attempt, datetime.UTC).strftime('%Y-%m-%dT%H:%M:%SZ')
    result = subprocess.run([f'faketime {attempted_datetime} ./stashPatchNOP'], shell=True, stdout = subprocess.PIPE)
    output_lines = result.stdout.decode('utf-8').split('\n')
    loot_item = output_lines[-2]
    loot[loot_item] = [loot_item, attempted_datetime, unix_seconds_attempt]
    unix_seconds_attempt += 1

print(f'Brute Force Cycles: {unix_seconds_attempt}')
print(f'Total Loot Items Found: {len(loot)}')

for key in loot:
    print(loot[key])

The output of ‘TimeForLoot.py‘:

jamie@VM-Linux-Ubuntu:~/Documents$ python3 ./TimeForLoot.py 
Brute Force Cycles: 1983
Total Loot Items Found: 255
["You got: 'Dreamkiss, Might of Traitors'. Now run, before anyone tries to steal it!", '1970-01-01T00:30:46Z', 1846]
["You got: 'Aqua, Destroyer of the Ancients'. Now run, before anyone tries to steal it!", '1970-01-01T00:20:58Z', 1258]
["You got: 'Lightbane, Crusader of Trembling Hands'. Now run, before anyone tries to steal it!", '1970-01-01T00:27:54Z', 1674]
["You got: 'Penance, Skull of Dark Souls'. Now run, before anyone tries to steal it!", '1970-01-01T00:14:03Z', 843]
["You got: 'Dreamshadow, Destroyer of Riddles'. Now run, before anyone tries to steal it!", '1970-01-01T00:29:54Z', 1794]
["You got: 'Oblivion, Bauble of Mountains'. Now run, before anyone tries to steal it!", '1970-01-01T00:30:34Z', 1834]
...

Going through the list of loot items we come across the golden ticket:

["You got: 'HTB{n33dl3_1n_a_l00t_stack}'. Now run, before anyone tries to steal it!", '1970-01-01T00:32:47Z', 1967]

Of course this is in the past (unix epoch + 1967 seconds). Here’s a quick history lesson for computers in 1967!

When is the next time this particular item will be available from the stash? A quick modification of the python script variable unix_seconds_attempt to equal the current unix epoch time can tell us:

jamie@VM-Linux-Ubuntu:~/Documents$ date --iso-8601='seconds';
2025-08-03T16:31:55+00:00
jamie@VM-Linux-Ubuntu:~/Documents$ python3 ./TimeForLoot.py 
...
["You got: 'HTB{n33dl3_1n_a_l00t_stack}'. Now run, before anyone tries to steal it!", '2025-08-03T17:01:13Z', 1754240473]
...

Running the executable in 29 minutes 18 seconds from now will give us the flag.

The alternative to all of the above? The CTF token is in plain text amongst all of the other items; index 97 (from the 0 index array of ‘gear‘), or offset 0x3c50 of ‘stash‘. During the static analysis process it was found, but in spirit of the challenge and as a learning exercise, it was ignored.

Submit:

Let’s submit it to confirm:

and on with the next one!